Troubleshooting#
Most things that go wrong in PacketBench go wrong with an exact string attached — in a terminal pane, in an amber dictation panel, in a health badge, or in a sanitised host-API message. Those strings are the index into this page, because they are the only part of a failure a reader can search for.
This page groups symptoms by the surface that produced them, names the cause,
and gives the workaround where one exists. Where a workaround cell reads —,
there is none, and the row says why.
Terminal panes#
Everything below is what a PTY pane writes into its own xterm buffer. The pane never surfaces a modal for these; if you did not read the pane, you did not see the error.
| What you see | What happened | Workaround |
|---|---|---|
[Session ended] in grey |
The child process exited, or you killed or restarted the pane (src/hooks/useTerminalSession.ts:322) |
Restart the pane. This is also written after a crash — see below |
Failed to start <CLI>: <message> in red, then Make sure '<cmd>' is installed and on your PATH. in grey |
The spawn itself failed (src/hooks/useTerminalSession.ts:421) |
Install the CLI, or pin an absolute path — see the allowlist note |
Command '<x>' is not allowed. Allowed commands: [...] |
The program is not on the PTY allowlist (src-tauri/src/commands/pty.rs:30, raised at :801) |
Use one of the listed programs. The allowlist is claude, codex, opencode, packetcode, ssh, and the shells |
Project path '<x>' is not a valid directory |
The workspace's projectPath no longer exists on disk (src-tauri/src/commands/pty.rs:841) |
Repoint the workspace, or recreate the directory |
| A pane that opens and never paints anything | A WebGL canvas was attached while the container was hidden or 0×0 | Switch to another workspace and back — the hidden→visible transition disposes and reattaches the addon |
| On macOS, a CLI that resolves in Terminal is "not found" in the packaged app | launchd hands a .app a minimal PATH — /usr/bin:/bin:/usr/sbin:/sbin — which contains no Homebrew, npm, or ~/.local/bin |
PacketBench already reconstructs PATH at startup (src-tauri/src/core/shell_path.rs). If your install directory is unusual, add it to a literal PATH= line in a shell rc file, or pin an absolute path |
The allowlist matches on the program name, not the whole command string
(src-tauri/src/commands/pty.rs:66, :798). A manually pinned absolute path
such as D:\projects\packetcode\bin\packetcode.exe is therefore accepted by
its basename, which is the supported way to run a CLI that is installed
somewhere PATH does not reach.
A silent pane end is not necessarily a clean exit#
A CLI that crashes on startup renders exactly like one you closed: the pane
writes [Session ended] and stops. There is no exit code, no error, and
nothing to act on.
The backend does the work and the frontend throws it away. commands/pty.rs
reads the child's status and emits a typed PtyExitPayload; the normaliser
for it, parsePtyExitPayload, is exported from src/lib/tauri.ts:227 and has
no production callers — its only references are in tests. The real
listener at src/hooks/useTerminalSession.ts:360 binds listen<unknown> and
discards the payload. This is recorded as a P1 defect in backlog.md, not as
a design decision.
The live case that found it: the vendored @openai/codex 0.147.0 binary
access-violates on startup with 0xC0000005. On Windows a .cmd-wrapped CLI
is spawned as cmd.exe /c codex.cmd
(src-tauri/src/commands/pty.rs:873-880), so cmd.exe starts successfully,
the PTY session is created, the real CLI dies milliseconds later, and the pane
closes quietly.
When a pane ends immediately and you did not close it, run the
CLI directly in a terminal before assuming PacketBench is at fault.
codex --version reproduces the crash above outside the app. Until the exit
payload reaches a listener, that is the only way to tell a broken CLI from a
clean exit.
Blank panes were a parser abort, not a missing feature#
xterm 6.0.0's bundled DECRQM handler (requestMode) throws ReferenceError: Can't find variable: i in minified builds, which aborts the entire parse. Any
output stream containing a DECRQM query then renders nothing.
opencode/opentui sends several on startup, which is why its panes were
blank while claude and codex — which do not query — were fine.
PacketBench registers a no-op CSI handler for both DECRQM forms
(src/hooks/useXterm.ts:68-69) so the broken built-in never runs. The CLI
receives no mode-support reply and handles that gracefully. Implemented;
if you still see a blank opencode pane on 0.13.1, it is not this.
SSH remote workspaces#
A remote workspace always launches ssh locally and runs the CLI on the other
end, so failures split cleanly into local-spawn failures and remote-shell
failures.
| What you see | What happened | Workaround |
|---|---|---|
Command 'x' is not allowed. on a machine with remote workspaces |
A local pane is trying to launch something unlisted. A remote workspace launches ssh, which is allowlisted |
Check which pane raised it; the remote path cannot produce this |
| A remote CLI is "not found" even though it is installed | The remote command is not run through a login shell, so a PATH exported from ~/.bash_profile never applies |
PacketBench prepends $HOME/.local/bin, $HOME/.npm-global/bin, $HOME/.cargo/bin, $HOME/.opencode/bin, the newest nvm Node bin, and /usr/local/bin (src/lib/ssh.ts:10; the sidecar's copy is src-tauri/src/commands/agent_sidecar/supervisor.rs:38). If your CLI is elsewhere, put it in a file a non-login shell reads, or pin an absolute path |
| Every tool call is slow, on Windows only | Windows OpenSSH has no ControlMaster — it needs Unix domain sockets — so every call pays a full handshake (src-tauri/src/core/execution.rs:96, :130) |
None — expected. On Unix the same code sets ControlMaster=auto with a socket under ~/.packetbench/ssh-cm/ and ControlPersist=60, so only the first call pays |
Refusing to launch against <host>: host key not verified. Pin it on the Servers page first. |
The Flight target's host record carries no fingerprint (src-tauri/src/commands/flight_attempts.rs:248) |
Settings → Remote Hosts, open the host, use Verify host key, save |
An amber banner reading N hosts without a pinned key |
Those hosts fall back to trust-on-first-use, so a first connect cannot detect an impostor (src/components/views/tools/ServersSettingsCard.tsx:167) |
Same fix. Existing setups keep working either way |
| The Memory pane looks like the wrong project | Remote scope keys are ssh:<serverId>:<remote path> and never match a local workspace |
Check the scope chip in the Memory header — it reads <server> · <folder> with a server icon on a remote workspace |
The refusal message says "Servers page"; the settings group is
labelled Remote Hosts (src/lib/settingsNavigation.ts:122). They are the
same card. The string has not been updated to match the label.
Dictation#
Dictation carries more failure machinery than any other surface, because a Bluetooth headset that walks out of range can block inside CPAL indefinitely and a bare spinner is a dead end.
| What you see | What happened | Workaround |
|---|---|---|
| An amber panel after 8 s of "Starting…" | The microphone has not opened. The panel explains the likely causes and offers Cancel (src/components/views/DictationView.tsx:17) |
Cancel, then check the device in Settings → Integrations & Data → Dictation |
| The start gives up and reports a failure after 15 s | The store's start watchdog fired, told the backend to drop the device, and reset (src/stores/dictationStore.ts:98) |
Reconnect the device and retry. Without this the store would keep isStarting true forever and push-to-talk would go permanently dead |
| An amber panel after 90 s of "Transcribing…" | Large models on long recordings genuinely take this long. The panel says so and offers Cancel (src/components/views/DictationView.tsx:18) |
Wait, or Cancel. Escape always reaches the backend |
| Transcription is abandoned with a timeout | The transcribe watchdog fired. Its budget is 20× the recorded length, with a 90 s floor for cold model load and a 15-minute ceiling (src/stores/dictationStore.ts:99-100, :122-126) |
Use a smaller Whisper model, or record shorter takes |
| Recording stops on its own and transcribes what it has | The maximum recording length was reached — 300 s by default, clamped by the backend to between 10 s and 1,800 s (src-tauri/src/commands/dictation/config.rs:59, :78) |
Raise the ceiling in Dictation settings. The clamp bounds retained PCM even if a key release is missed |
| The device dies mid-recording | Dictation stops and transcribes anyway, so the words already spoken survive; the device error comes back as a warning on the result | None needed — this is the salvage path |
| The device dies while opening | The in-flight open is abandoned and a salvage stop runs (src/stores/dictationStore.ts:209-244) |
Reconnect and retry |
A non-fatal dictation:warning stall notice |
The capture has delivered no new frames for 60 emitter ticks, roughly two seconds (src-tauri/src/commands/dictation/audio.rs:80, :741) |
Keep going. The capture is deliberately not torn down — a stall can recover, and tearing it down would cost you the buffer |
The capture was too short to transcribe. Hold the dictation key until the waveform moves. |
The capture did not reach the minimum length (src-tauri/src/commands/dictation/audio.rs:930) |
Hold the key longer |
| A wedged "Transcribing…" spinner | Anything above that did not resolve | Escape. Cancel always reaches the backend and always resets locally; a cancel with nothing in flight is a state reset and will not wipe a transcript you are still reading |
The stall watch counts frames, not loudness — StallWatch::observe
compares the cpal frame counter against the previous tick and never reads
amplitude (src-tauri/src/commands/dictation/audio.rs:92-125). Speaking
quietly does not trip it; a headset that goes silent without raising a stream
error does, and so does a capture that never started at all, because the
counter sits at zero from the first tick. It fires once per capture rather
than once every 33 ms.
MCP servers#
Diagnose on a configured server spawns it, performs the MCP handshake,
calls tools/list, and shuts it down. It reports one of four states, and
those four are not four grades of the same thing. The last two rows are errors
raised before any probe runs.
| Badge | What it means | Workaround |
|---|---|---|
connected |
Handshake and tools/list both succeeded. Latency and the tool list are shown (src-tauri/src/commands/mcp.rs:326) |
— |
degraded |
Checked, and unhealthy. The process started but the handshake or listing failed; the error text and a measured latency are shown (:341) |
Read the message — it is the server's own error, not PacketBench's |
failed |
The process could not be spawned at all, or the entry's type is not a transport PacketBench recognises (:280, :352) |
For an unknown transport the message names the value it read and lists stdio, http, sse. That is a typo in your config, not a network problem |
notProbed |
Health unknown. Nothing was sent to the server, so nothing was measured (:263) |
None — no local probe exists for this transport |
MCP server is not configured in the selected scope |
The name does not exist in the global/project scope you are diagnosing (:240) |
Switch scope, or re-add the server |
MCP stdio server is missing a command |
A stdio entry with no command field (:295) |
Fix the entry in ~/.claude/settings.json or the project .mcp.json |
notProbed is not a failure#
The doctor is a local stdio prober by construction, and this build has no HTTP
MCP client to probe with. An http or sse server therefore reports
notProbed and carries no latency, because a number there would imply
something was timed.
The distinction is load-bearing and is pinned by a test
(src-tauri/src/commands/mcp.rs:604). Every non-stdio server used to be
reported as degraded without a single byte being sent to it. degraded
reads as "we checked and it is unhealthy", so every healthy remote server
looked broken — which teaches you that the indicator means nothing and to
ignore a real degraded when one appears. A malformed type is separated out
for the same reason: it used to be swept into the same "remote, not probed"
answer, which sent you looking for a network problem instead of a typo.
Git hosts#
Host API errors are sanitised before they reach you. The raw response body is
logged, never displayed, because it can echo tokens and private repository
data (src-tauri/src/core/git_host.rs:931).
| Status | Message | Workaround |
|---|---|---|
| 401 | <host> API error 401: unauthorized — check your <host> token |
Re-enter the token in Settings → GitHub |
| 403 | … forbidden — you may lack permissions or be rate-limited |
Check the token's scopes first; a rate limit clears on its own |
| 404 | … not found — the resource may not exist or may be private |
Usually a missing scope on a private repository rather than a wrong URL |
| 422 | … validation failed — check your request parameters |
The host rejected the payload. Nothing was written |
| 429 | … rate limited — try again later |
Wait |
| Other 4xx | … client error |
— |
| 5xx | <host> API error <code>: <host> server error — try again later |
Wait |
<host> is resolved from the URL that actually answered: api.github.com
becomes GitHub, any other host becomes its own hostname, and a hostless URL
becomes Git host (src-tauri/src/core/git_host.rs:920-929).
Earlier documentation states that the prefix reads "GitHub API
error" for Gitea responses too. That was true when the label was hardcoded in
commands/github.rs; it is not true at 0.13.1. host_label_from_url was
moved into core/git_host.rs so the agent tools in core/tool_github.rs
could reuse it, and it names the host that answered. A Gitea instance at
git.example.com now produces git.example.com API error 401: unauthorized — check your git.example.com token.
Installers and first launch#
Nothing PacketBench ships is code signed. Every symptom in this table follows from that one fact — see Release status for what is and is not configured.
| What you see | What happened | Workaround |
|---|---|---|
| Windows SmartScreen: "Windows protected your PC" | The installer carries no Authenticode signature. src-tauri/tauri.conf.json has no bundle.windows block at all |
More info → Run anyway. Check the installer's SHA-256 against the table in CHANGELOG.md first — that is the only integrity check available |
macOS refuses to open the .app on first launch |
The bundle is unsigned and un-notarized, so Gatekeeper blocks it. bundle.macOS carries only minimumSystemVersion |
System Settings → Privacy & Security. Right-click → Open no longer works for a downloaded unsigned app on current macOS |
A locally built .app is quarantined |
Same cause, local build | xattr -cr /Applications/PacketBench.app — local testing only. Never ship this instruction to anyone downloading a build |
A clean Mac fails the build inside whisper-rs-sys |
The dictation module build-depends on CMake, which does not ship with the Xcode Command Line Tools | brew install cmake. It is mandatory, not optional |
A standalone target/release/packetbench.exe reports the sidecar down |
The per-triple Node binary was never fetched. src-tauri/build.rs:17 emits a cargo warning, not an error, so the build succeeds with a dead sidecar |
Run pnpm fetch-node and rebuild. Installs made through the MSI or NSIS bundle are unaffected — the bundler places the runtime for you |
| The app launches but a route shows stale or missing UI after an upgrade from a PacketADE build | WebView2 keys its profile by bundle identifier, and the 2026-08-26 rename moved it from com.packetade.desktop to com.packetbench.desktop. Twelve localStorage keys, including unsent composer drafts, are stranded in the old profile |
None. The old profile stays on disk but nothing reads it. This is an accepted consequence of the rename; upgrades between two PacketBench versions are unaffected |
Related#
- Release status — why nothing is signed, and what has actually been proved on a packaged install.
- Install & first run — building from source, which is the path these symptoms assume.
- Workspaces & terminals — the pane, shell and session model behind the terminal table.
- SSH remote workspaces — host pinning, the remote
PATHprefix, and what is gated off remotely. - Dictation & analytics — capture, delivery safety, and the settings each timeout reads.
- MCP hub — trust profiles, and which agent rows can use a server at all.
- Where data lives — the files to inspect when a symptom looks like lost state.