Living document. Update this, don't create new versions.
Common issues and solutions for AgentWire.
# Auto-diagnose and fix common issues
agentwire doctor
# Show what would be fixed without making changes
agentwire doctor --dry-run
# Auto-fix everything without prompts
agentwire doctor --yes
# Walk ONLY the push-to-talk path (fast, no SSH waits): mic, STT shim,
# portal/tunnel, tmux+PTT — each pass/fail with a fix line when red
agentwire doctor --voice
# Check network/service health
agentwire network statusagentwire doctor --voice walks the live voice loop end to end and tells you
which link is broken with a next step:
| Stage | Red means | Fix |
|---|---|---|
| Mic / audio capture | ffmpeg missing, or (macOS) no input device — mic permission revoked | Grant the mic in System Settings → Privacy → Microphone, or brew install ffmpeg |
| STT process | the Moonshine :8101 shim (default tier) or your custom shim isn't responding |
agentwire stt start (the portal also auto-starts the default shim) |
| Tunnel / portal reachability | the portal isn't up, or a required reverse tunnel is down | agentwire portal start / agentwire tunnels up |
| tmux wiring + PTT binding | tmux is missing (no way to deliver keystrokes); host ⌥Space binding absent (informational) | brew install tmux; for host PTT copy examples/hammerspoon-ptt/init.lua |
Break one dependency and exactly that stage goes red while the others stay green — so the broken link is always obvious.
Cause: Python version too old.
Fix: Upgrade Python to 3.10+
# macOS
brew install pyenv
pyenv install 3.12.0
pyenv global 3.12.0
# Ubuntu
sudo apt install python3.12Cause: Ubuntu's PEP 668 protection prevents global pip installs.
Fix: Use a virtual environment
python3 -m venv ~/.agentwire-venv
source ~/.agentwire-venv/bin/activate
echo 'source ~/.agentwire-venv/bin/activate' >> ~/.bashrc
pip install agentwire-devCause: Installation directory not in PATH.
Fix: Add to PATH
# Add to ~/.bashrc or ~/.zshrc
export PATH="$HOME/.local/bin:$PATH"
# Then reload
source ~/.bashrc # or source ~/.zshrcCause: ffmpeg not installed (required for audio recording).
Fix:
# macOS
brew install ffmpeg
# Ubuntu/Debian
sudo apt install ffmpegCause: Self-signed certificates not trusted by browser.
Fix:
- Generate certificates:
agentwire generate-certs - Open https://localhost:8765 in browser
- Click "Advanced" > "Proceed to localhost (unsafe)"
- Browser will remember the exception
Check status:
agentwire portal statusCommon causes:
| Cause | Fix |
|---|---|
| Port in use | lsof -i :8765 to find process, kill it |
| Missing SSL certs | agentwire generate-certs |
| tmux not running | tmux new -d -s test && tmux kill-session -t test |
Start with debug output:
agentwire portal serve # Runs in foreground with logsCause: Browser blocking mixed content or SSL issues.
Fix:
- Ensure using
https://nothttp:// - Accept the SSL certificate warning first
- Check browser console for specific errors
Check TTS server:
agentwire tts statusIf not running:
agentwire tts startTest TTS directly:
agentwire say "Hello world"Check configuration:
# ~/.agentwire/config.yaml
tts:
backend: "custom" # default = in-process Kokoro (no service to debug;
# check `agentwire tts status` for model state)
url: "http://localhost:8100"
default_voice: "default"
options:
backend: kokoro # bundled-shim engine: kokoro | chatterbox |
# chatterbox-streaming | zonos-*Default tier (Kokoro) quirks: the model lives in ~/.cache/kokoro_onnx/
(agentwire tts warm pre-downloads it; delete the directory to force a fresh
download). If you hear the robotic browser voice, the model is still
downloading or failed — check the portal toast or /api/voice-status.
Python 3.14+ has no kokoro-onnx wheels yet; the portal logs a warning and
stays on speechSynthesis. If another package installed the real phonemizer,
it can clobber kokoro-onnx's phonemizer-fork (same module name) —
reinstall in a clean venv.
See ../voice/shim-contract.md for the contract and ../voice/tts-self-hosted.md for the full engine matrix.
Default tier (stt.backend: default): recognition happens in the browser.
Use Chrome (the blessed browser); check the mic permission and that the page
is on localhost or HTTPS (secure context). There is no server component.
Custom tier (stt.backend: custom): the portal uploads audio to your shim.
agentwire stt status # probe the shim
# Test transcription manually
ffmpeg -f avfoundation -i ":default" -t 5 -ar 16000 -ac 1 test.wav
curl -s -X POST http://localhost:8101/transcribe -F file=@test.wavmacOS: Check System Preferences > Privacy & Security > Microphone
Linux: Check arecord -l for available devices
Configure specific device:
# ~/.agentwire/config.yaml
audio:
input_device: 0 # Device index, or "default"Check tmux:
tmux list-sessionsCommon causes:
| Cause | Fix |
|---|---|
| tmux not installed | brew install tmux or apt install tmux |
| Session name invalid | Use alphanumeric, -, _ only |
| Path doesn't exist | Create directory first |
List available sessions:
agentwire listCheck session exists in tmux:
tmux list-sessionsTry attaching directly:
tmux attach -t session-nameCapture output manually:
tmux capture-pane -t session-name -pCheck pane count:
agentwire info -s session-nameTest SSH directly:
ssh machine-id # Should connect without passwordFix SSH key auth:
ssh-copy-id user@hostCheck machine config:
agentwire machine listCheck tunnel status:
agentwire tunnels statusCreate tunnels:
agentwire tunnels upVerify port is listening:
lsof -i :8100 # Check TTS portCause: SSH connection dropping.
Fix: Add to ~/.ssh/config:
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h-%p
ControlPersist 600
Test command:
agentwire safety check "your command here"View recent blocks:
agentwire safety logs --tail 20Check pattern that matched:
The safety check output shows which pattern blocked the command.
Check status:
agentwire hooks statusInstall hooks:
agentwire hooks installagentwire doctor prints [!!] ... BAD LINK and names the target when a
~/.claude/hooks/* symlink is dangling, outside the installed tool, or inside a
git worktree/temp tree. While that is true every session runs that tree's
un-deployed code. Recover with one command from the installed tool (never from
a worktree — it refuses there and says how to fix):
agentwire hooks installRoot cause was a unit test: the hook dirs were frozen to the real ~/.claude at
import, so an install_hooks(allow_foreign=True) test repointed the real links at
its checkout. Now the dirs resolve at call time, every machine-global symlink goes
through hooks_cli._link_into_claude (refuses a target outside the canonical
install, before touching the existing link), and the test guard fails any test that
writes under the real ~/.claude or ~/.agentwire. The guard is detect-after-the-fact (an audit hook records, it does not block); prevention is the call-time dirs and _link_into_claude.
Check hook installation:
ls ~/.claude/hooks/idle-handler.shVerify parent is configured:
Check .agentwire.yml in the project directory:
parent: agentwire # Must be set for cross-session notificationsThere is no cooldown/rate-limit mechanism in the current idle-handler.sh — every genuine idle event fires a notification. If notifications fire more often than expected, look for the agent re-entering an idle state repeatedly (e.g. a flapping tool call) rather than a rate-limiter to reset.
A turn that ends with a background shell or monitor still running is paused,
not done: the harness re-wakes the session when the task reports. The idle hook
asks agentwire idle-gate first and swallows that idle (idle-gate: action=defer in /tmp/claude-hook-debug.log); the idle that follows the last
task's report carries zero pending and sends the ordinary notice. This holds for
every consumer of idle in the hook: the parent notice, the worker-pane
summary/reap, and the scheduled-task summary//exit pass. Because the cohort
ledger's resolved_idle placeholder is the same kind=idle message, a child
with a pending task no longer reads as "went idle without reporting" either.
Signals (agentwire/background_tasks.py, measured on a real session): the
pane's turn-end line ✻ Worked for 3s · done 7:37 PM · 1 shell still running
is authoritative, read at its LIVE position only — the block directly above the
last input box (it wraps over several rows on a narrow pane; the footer
⏵⏵ … · 1 shell truncates and only corroborates). Older turn lines and a
footer quoted in scrollback are history and ignored. The transcript is the
fallback when the pane is unreadable: a start is a user row with
toolUseResult.backgroundTaskId (Bash) / taskId (Monitor), an end is a
queue-operation <task-notification> that carries a <status> (a Monitor's
per-event notices have none). A transcript task the pane no longer shows is
stale (from before an /exit + --resume) and counts as zero.
The rule: unknown means HOLD, never reap. A task-kind word the parser has
not seen next to "still running", a footer cut off (including right after the
digit: · 1), any unparsable digit-bearing segment, a pane with no input box
(tall draft, plan/permission/resume dialog) that still shows pending-looking
text, or a gate crash on a reap path reads as uncertain, which every consumer
treats as pending. Two cases stay distinct: an unparsable fragment holds, but a
clean N <noun> footer segment whose noun is not a task kind (2 MCP servers failed, 3 files changed) is simply not a task. A bare shell with no signal at
all behaves as before.
A gate that gives no answer holds too. The hook decides in shell, before the
gate runs, whether this idle reaches a kill (pane index > 0, or a scheduled-task
context file exists). The gate call is bounded (AGENTWIRE_GATE_TIMEOUT, default
8s) and counts only if it exits 0 with a parseable defer|proceed|qualify. Exit
1, a traceback, kill -9, a timeout, garbage, or a missing/old binary on a kill
path holds (logged, gate_unusable event, retried on the next idle); only a
plain pane 0 falls through to the notice. A persistently failing gate therefore
keeps worker panes alive silently, so agentwire doctor flags [!!] when the
gate failed 3+ times in the last hour.
Declared trade-offs. A footer-only reading (no turn-end line) with a noun off
the allowlist (· 1 job) reads 0/ok: the turn-end line covers the normal case.
A digit-bearing segment that is not N noun (· 95%) reads uncertain
(informational; not seen on real mode lines). A stuck monitor with a long timeout
gets ONE qualified notice to its pane-0 parent; a worker or scheduled pane stays
held until the task ends, then is reaped normally — the owner signal is doctor
("N task(s), Xm, qualified"). Verified on real Claude 2.1.302: Esc and Ctrl-C do
not clear "still running" (the task survives), so the pane stays truthful. The worker-pane summary/kill --pane and the
scheduled-task /exit fail to KEEP; the parent notice is held, then released
by the bound. Only "nothing readable at all" (no agent pane, no transcript)
behaves as before the gate.
The bound releases a notice, never a kill. After 30 minutes of deferral one
qualified notice is sent — is idle, but N background task(s) still running (waited 30m; not necessarily finished) — to the parent (pane-0 session: by the
hook on the next idle, or by the watchdog idle_deferred stage if none comes;
worker pane: by the stage, to its owning pane 0). A worker pane and a scheduled
task stay held for as long as the task may be running; a live task is not ended
by a timer. qualified is recorded only after the enqueue succeeded (a failed
one retries next tick); a session with nobody to tell is marked unroutable,
never "sent". A session incarnation is the conversation id the hook supplies: a different id
restarts the clock, and a call that carries an id is NEVER restarted by a gap (so
neither a slow wake nor a restart re-arms qualified). Only a call with no id
falls back to a gap, and that gap is the bound itself.
Without the watchdog loaded a stuck task that never wakes its session is never
told about: agentwire doctor flags that as [!!]. State: ~/.agentwire/idle-deferred/, events:
~/.agentwire/idle-deferred-events.jsonl; agentwire doctor lists held
notices.
For workers (panes 1+): Notifications go to pane 0 automatically.
For orchestrators (pane 0): Notifications go to the resolved parent: the session recorded as created_by at creation time, falling back to .agentwire.yml's parent: field if unset.
Check current session/pane:
echo $AGENTWIRE_SESSION # Current session name
echo $TMUX_PANE # Current pane (e.g., %5)Cause: ensure waits for both the summary file AND the context file (~/.agentwire/tasks/{session}.json) to be deleted. The context file deletion happens on the hook's second idle pass, which requires another 60-second idle cycle after the summary is written.
Fix: Wait for the next idle cycle (~60s). Check /tmp/claude-hook-debug.log for TASK: second idle messages. If the hook isn't firing, the agent may still be processing.
Force unblock (if stuck):
# Check if context file still exists
ls ~/.agentwire/tasks/
# Manually delete context file (ensure will proceed)
rm ~/.agentwire/tasks/session-name.json| Command | Audio | Use Case |
|---|---|---|
agentwire say |
Yes (TTS) | User-facing messages, completion announcements |
agentwire msg send |
No (text only) | Polite peer report-back, dropped into the recipient's inbox |
agentwire notify-parent --to |
No (text only) | Worker → orchestrator status, injected as text |
Idle hooks route text alerts to the orchestrator (no audio) to avoid spam when multiple panes go idle.
Cause: WebGL not available, falling back to canvas.
Fix: Use a browser with WebGL support (Chrome, Firefox, Edge).
Check what's running:
agentwire portal status
agentwire tts status
tmux list-sessionsKill unused sessions:
agentwire kill -s unused-sessionSymptom: agentwire new -s name --posture bypass creates a tmux session but Claude never starts - you just see a bash prompt.
Cause: System prompt (from roles) contains characters that break shell escaping when sent via tmux send-keys.
Common triggers:
- Newlines in role instructions
- Unescaped quotes
- Very long command lines that wrap incorrectly
How it manifests:
% claude --append-system-prompt "line1
quote> line2" # Bash waiting for closing quote
Solution: This was fixed by writing the system prompt to a temp file instead of embedding it in the command line. If you see this issue:
- Make sure you're running the latest version:
agentwire rebuild - Check role files for unusual characters
- See
shell-escaping.mdfor technical details
Cause: Very long commands wrap in the terminal, making the output hard to read.
Not actually broken: If Claude starts (check with pgrep -f claude), it's working fine - just display weirdness.
- Run diagnostics:
agentwire doctor - Check logs: Portal logs are in the portal tmux session (default:
agentwire-portal) - Report issues: https://github.com/dotdevdotdev/agentwire-dev/issues
When reporting issues, include:
- Output of
agentwire doctor --dry-run - Output of
agentwire --version - Your OS and Python version
- Steps to reproduce
Symptom: Claude Code sessions occasionally get stuck on a "0 token" action — the model produces no output and the session hangs (e.g., "Perambulating..." indefinitely).
Most common triggers:
- Voice-orchestrator sessions delegating to workers
- Mid-interaction during Chrome browser automation
- Right after receiving notifications/alerts
Workaround: nudge the session with agentwire send -s name "continue". May need 2–3 nudges. Kill the session if its task is otherwise complete.
This appears to be a Claude Code-side stall, not an agentwire issue.