---
name: windows-launch-diagnostics
description: When launching a Windows .exe (especially a server, daemon, or background service) for the first time, always capture stdout/stderr to files. Never trust Start-Process without redirection — silent crashes are the default failure mode. Use this pattern for any Windows service-style binary launch where you need to diagnose startup failures.
---

# Windows binary launch diagnostics — the capture-everything pattern

## The core rule

**Never launch a Windows .exe with `Start-Process $exe` (bare) when you're trying to figure out why it's not working.** If the binary crashes on startup, the error goes to a console window that opens and closes in <100ms, or to nowhere at all. You'll be left guessing. Always redirect stdout and stderr to log files on disk, then read those logs after a wait.

## The pattern

```powershell
$exe     = "C:\full\path\to\whatever.exe"
$outLog  = "$env:USERPROFILE\Desktop\whatever-stdout.log"
$errLog  = "$env:USERPROFILE\Desktop\whatever-stderr.log"

# Clean stale logs
Remove-Item $outLog, $errLog -ErrorAction SilentlyContinue

# Defensive: kill any prior instance
Get-Process whatever -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Seconds 1

# The launch — three flags you almost always want together:
#   -PassThru            : returns the process object so you can read PID/state
#   -NoNewWindow         : redirects stdout/stderr to OUR files (not a new console)
#   -WorkingDirectory    : the exe's home dir (some apps read sibling files at startup)
#   -RedirectStandardOutput / -RedirectStandardError : capture both streams
$proc = Start-Process $exe -PassThru -NoNewWindow `
    -WorkingDirectory (Split-Path $exe) `
    -RedirectStandardOutput $outLog -RedirectStandardError $errLog

Write-Host "PID = $($proc.Id)"

# Wait long enough for the binary to either bind its port or fail trying
Start-Sleep -Seconds 10

# Verify whatever success condition you expected
$listen = Get-NetTCPConnection -State Listen -LocalPort <PORT> -ErrorAction SilentlyContinue
if ($listen) {
    Write-Host "OK — listening on port <PORT>"
} else {
    Write-Host "FAILED — reading captured output:"
    Write-Host "=== STDOUT ==="
    if (Test-Path $outLog) { Get-Content $outLog } else { Write-Host "(empty)" }
    Write-Host "=== STDERR ==="
    if (Test-Path $errLog) { Get-Content $errLog } else { Write-Host "(empty)" }
    $still = Get-Process whatever -ErrorAction SilentlyContinue
    if ($still) {
        Write-Host "Process still alive but didn't bind — hung during init"
    } else {
        Write-Host "Process exited — crashed on startup"
    }
}
```

## Why each flag matters

| Flag | What happens without it |
|------|------------------------|
| `-PassThru` | You can't inspect the launched process's state after Start-Process returns |
| `-NoNewWindow` | A new console window opens for the child process. If the child crashes, the window closes before you can read it. With -NoNewWindow, stdout/stderr are inherited and you can redirect them. |
| `-WorkingDirectory (Split-Path $exe)` | Most server-style exes read sibling files (config, templates, certs) relative to CWD. If you launch from PowerShell's CWD, it can't find them. Setting CWD to the exe's directory is the safe default. |
| `-RedirectStandardOutput` / `-RedirectStandardError` | Without these, output either goes to a closing-too-fast console or nowhere. With these, output lands in files you can `Get-Content` later. |

## When to use a visible console instead

You DO want a real visible window if:
- The binary is meant to be a long-running interactive CLI tool (REPL, TUI app, chat client)
- You want the user to be able to read error output themselves and Ctrl+C to stop it
- The binary takes input from stdin

In that case, swap `-NoNewWindow` for `-WindowStyle Normal` and accept that crashes will be invisible. If you need a middle ground, use `Start-Process ... -RedirectStandardError $errLog` (no `-NoNewWindow`) — the window opens visibly, but stderr still goes to the file.

## Reading the captured output

After waiting:
1. **Both logs empty, process dead** → exe itself can't start. Likely missing runtime DLL, AV block, corrupt binary, or wrong arch (32 vs 64-bit). Reinstall or check Windows Event Viewer (eventvwr.msc → Windows Logs → Application).
2. **STDOUT shows banner + log lines, process dead** → binary started but hit a fatal config/init error. Read the log tail for the error message.
3. **STDERR shows Python/Rust/Node traceback** → unhandled exception. The traceback names the missing module, file, or condition.
4. **Both logs have content, process alive, port not bound** → binary is running but stuck during init. Wait longer, or check if it's writing to a different log file (not stdout).
5. **Everything looks fine, port bound** → success. Don't keep the launch wrapper open.

## Processes that exit after running (the "stopped, not crashed" failure mode)

The classic silent-crash-on-startup is one failure mode. The other, equally invisible one: **the process starts cleanly, runs for a while, and then exits cleanly — no traceback, no error log, no abnormal exit code.** Symptoms:

- `Get-Process foo` returns the PID right after launch → returns empty N minutes later
- Port was bound → port is now free
- stderr has only the startup banner (`Started server process [...]`, `Application startup complete.`, `Uvicorn running on http://... (Press CTRL+C to quit)`), no error traceback
- exit code is 0 if you can catch it (the process didn't crash, it was stopped)

**This happened to `fcc-server.exe` (Uvicorn-based FastAPI app) on 2026-07-16.** It launched, served ~14 health polls and one model-list request, then vanished after ~60 seconds. The status card in Agentic OS kept showing "Live" because the card's poll came in fast enough that at least one always landed before the process died.

**How to detect:**

```powershell
# After launch, sample process + port state on a loop, not a single check
$alive = $false
for ($i = 1; $i -le 60; $i++) {
    Start-Sleep -Seconds 5
    $proc = Get-Process foo -ErrorAction SilentlyContinue
    $listen = Get-NetTCPConnection -State Listen -LocalPort 8080 -ErrorAction SilentlyContinue
    if ($proc -and $listen) {
        Write-Host "[t=${i}s] alive PID=$($proc.Id), port 8080 listening"
        $alive = $true
    } else {
        Write-Host "[t=${i}s] GONE — proc=$($proc -ne $null), port=$($listen -ne $null)"
        break
    }
}
if ($alive) { Write-Host "Survived 5 minutes — looks healthy" }
```

**Likely causes when a server exits cleanly mid-run:**

- **Watchdog timer in the app** (e.g., Uvicorn's `--timeout-keep-alive`, or an app-level health check that triggers self-shutdown after N seconds without traffic)
- **A parent process is killing it.** If you launched from a PowerShell window that closes, or from a Scheduled Task with `StopIfGoingOnBatteries`, the child can receive a stop signal. Always launch via a method that survives parent exit (Scheduled Task with proper options, `Start-Process -WindowStyle Hidden`, or a Windows Service wrapper like NSSM).
- **Idle-exit by design.** Some dev-mode servers exit after N seconds of no traffic. Look for CLI flags like `--keep-alive`, `--no-exit`, `--dev-mode=false`.
- **The exe is actually a wrapper that runs once and exits.** PyInstaller / `uv tool` bundles often have a "main entrypoint" that returns after one unit of work.

**The fix is usually one of:**
- Run it as a Windows Service via NSSM or `sc.exe create` so it has a stable parent
- Schedule it via Task Scheduler with `AtStartup` trigger and `RestartOnFailure`
- Find the app's `--keep-alive` / `--no-idle-exit` flag and pass it

**Don't trust "Live" status badges from a UI that polls a health endpoint.** The UI sees the server while it's up; if it dies during the user's chat attempt, the UI still says "Live" until the next poll fails. Always confirm the actual chat/command path with a real request, not a health probe.

## Common Windows-specific gotchas

- **`Start-Process` does not throw if the exe doesn't exist.** It silently returns a process object that immediately exits. Always verify the path exists with `Test-Path` first.
- **Antivirus (Defender, CrowdStrike, etc.) can silently quarantine newly-spawned exes.** Check `C:\ProgramData\Microsoft\Windows Defender\Quarantine\Quarantine` or your AV's equivalent if the launch succeeds but the process disappears within 1–2 seconds.
- **`-NoNewWindow` does NOT work with all exes.** Some Windows console apps explicitly request a new console. If `-NoNewWindow` makes the launch fail, drop it.
- **UAC.** If the exe needs elevation, Start-Process won't prompt. You'd need `-Verb RunAs` (which itself prompts) or relaunch from an elevated shell.

## Pair with the windows-terminal-settings skill

This skill is about diagnosing why a server didn't start. The `windows-terminal-settings` skill is about why your settings don't stick. Different failure modes, same principle: **always capture state so you can read it later instead of guessing.**