Skip to main content

Troubleshooting

Start every investigation with the same two commands:

harness doctor # is the config loaded, the daemon reachable, anything degraded?
harness describe NAME # state, restarts, last exit, flapping, schedule

Then find the symptom below.

The agent is running but does nothing​

harness list shows ● running, and nothing happens. This is the most common failure, because it looks healthy. Attach and look before changing anything:

harness attach NAME --ro

Usual causes, most likely first:

  • It is waiting at a prompt nobody can see. First runs of Claude Code ask you to log in and to trust the folder, and an agent can stop to ask for tool approval. Attach read-write (harness attach NAME), answer the prompt, and detach with Ctrl-b d. For unattended work, run the agent once by hand in the same workdir first (first agent).

  • Its credentials expired or never arrived. A logged-out agent keeps running and fails every request. Check as the same user the daemon runs as. For Claude Code:

    claude auth status

    If you pass keys through an env_file, check the path. A missing env_file is silently skipped, so a typo means the agent starts with no key at all. After fixing either, harness restart NAME, because env files are read at start.

  • It never received its events. An always-on Switchboard worker idles until a doorbell arrives. If none arrive:

    • Is the channel enabled? Crush needs --channels server:NAME or channel_enabled: true, from a build that has the Streamable HTTP channel fix (details). Listing the server under mcp alone does not push.
    • Did the MCP server connect? An empty $SWITCHBOARD_TOKEN drops the Authorization header, and the server rejects the connection. Attach and check the agent's MCP status.
    • Is another session swallowing the doorbell? Switchboard rings one session per todo. An interactive session, or a second copy of the worker on another machine, using the same endpoint can take the ring and do nothing. See one channel consumer per server.
    • Does it drain on startup? Doorbells sent while the worker was down were dropped; their todos are still pending. Switchboard re-rings them, but only after about 5 minutes, then 20 minutes, 1 hour and 6 hours, and it stops after a fixed number of attempts. The worker's instructions should call claim_next until empty when it starts.
  • It's a Claude Code channel consumer that was never loaded as a channel, or is parked at a prompt. Without --dangerously-load-development-channels server:switchboard, Claude Code drops every doorbell silently while Switchboard logs it as delivered. With the flag, it waits for an interactive confirmation at every startup until someone attaches and answers. See the Claude Code status.

The harness bills my API account, or starts at a login prompt​

Both come from Claude Code picking a different credential than you meant. It takes the first one it finds, in this order: ANTHROPIC_AUTH_TOKEN, ANTHROPIC_API_KEY, an apiKeyHelper, CLAUDE_CODE_OAUTH_TOKEN, then your /login.

  • API charges for runs you expected on your subscription. An ANTHROPIC_API_KEY is set somewhere the harness inherits it: its env_file, the daemon's unit or plist, or the shell that started the daemon. It outranks a subscription token and your login, and a one-shot (claude -p) uses it without asking. Remove it, put CLAUDE_CODE_OAUTH_TOKEN in the env_file instead (or rely on the Keychain on macOS), and harness restart NAME.
  • A login prompt nobody answers. The harness has no credential it can reach: on Linux or a headless box there is no Keychain, and on macOS a daemon outside your GUI session cannot read it. Put a claude setup-token token in the env_file.

Check what a harness actually got, as the daemon's user:

claude auth status --text

or run /status in harness attach NAME. See Claude Code authentication for the three modes.

It keeps restarting​

harness list shows ◐ degraded or ◌ restarting, and the restart count keeps climbing, or it has already stopped at ✖ failed.

harness logs NAME --raw --lines 40
harness describe NAME # last_exit, flapping
  • last_exit -1 with no output at all. The process never started, and almost always the executable is not on the daemon's PATH. A service gets a minimal PATH, and a harness's env_file can't fix the executable lookup. Current builds don't log this spawn error, so check the path yourself:

    command -v claude crush codex # where they live in your shell
    systemctl --user show harness -p Environment # the PATH the daemon has (Linux)

    Add the missing directory to Environment=PATH=… in the unit, or to EnvironmentVariables in the plist, then restart the service. See the service guide.

  • A generic harness exits immediately. args go to sh, so a bare args = ["/usr/local/bin/thing"] asks sh to read that binary as a script. Use args = ["-c", "/usr/local/bin/thing --flag"].

  • A flag the CLI rejects. The agent prints its usage and exits 1. Read it with harness logs NAME --raw: for an agent harness, the default view shows agent activity, so a failure before any session started leaves only note lines there.

  • A real crash loop. After three exits within ten seconds the harness is flapping, and restarts back off.

What to do right away:

  • Stop anything looping, especially a metered agent:

    harness stop NAME

    The daemon stops on its own eventually: after more than 5 consecutive failures (non-zero exits from runs shorter than 5 minutes) it parks the harness in failed (✖ failed in harness list, an error in harness doctor). Stopping it yourself saves those last attempts.

  • Once it's fixed, clear the state: harness restart NAME. That also clears a failed state.

  • Prevent the next one: use restart = "on-failure" with a restart_delay of 30 seconds or more for agents (why).

A schedule didn't fire​

Check whether the daemon knows about the schedule at all:

harness jobs # every scheduled harness: schedule, next run, last run, failure streak
harness runs NAME # what each window did: success, failed, skipped, missed…
harness describe NAME # the cron spec and the absolute next run
  • It isn't in harness jobs. The config didn't load, or the harness is in a drop-in the daemon hasn't read. harness doctor shows a parse error with its file and line. For drop-ins, run harness reload (see the config didn't reload).
  • The machine was asleep or the daemon was down at that time. The window was missed. harness runs NAME shows a missed record, the daemon log has a scheduled run MISSED warning, and nothing ran because catch_up defaults to false. Set catch_up = true for work that should run once on wake.
  • It fired at the "wrong" time. Without a prefix, a schedule uses the daemon's local time zone. harness describe shows the next run; compare it to what you expected, and pin the zone with CRON_TZ=UTC (why).
  • The previous run was still going. With on_overlap = "skip", the default, a window that fires mid-run records skipped and does nothing. A run that never ends skips every later window. harness runs NAME shows the skipped records, and harness jobs shows the stuck run as running #N. Set a timeout.
  • It fired, and the run failed fast. harness runs NAME shows failed with its exit code, and harness logs NAME --run N shows what it did. Work through it keeps restarting for the cause.
  • It ran on a different machine. A synced config fires on every host running a daemon. Keep scheduled harnesses in a per-host drop-in directory (details).

To rule out the schedule entirely, run it now and watch:

harness trigger NAME --wait
echo $? # the run's exit code; 124 = timed out, 75 = skipped (a run was in flight)

The config didn't reload​

  • The file has an error. A config that fails to parse is not applied. The daemon keeps running the last good config. harness doctor shows the error with its file and line; fix it and save again.
  • You changed a drop-in. Only harness.toml itself is watched. Files in harness_d need harness reload.
  • You changed a running harness. Reload applies to what the daemon will run. A running harness keeps its old settings until it restarts, so an edit never kills an agent mid-task. harness describe NAME shows config — changed — restart to apply. Run harness restart NAME.
  • You changed an env_file or a prompt_file. Env files are read when a harness starts, so run harness restart NAME. Prompt files are read at every run, so the next run picks the change up with no action.
  • Watching is off. [daemon] watch_config = false, or HARNESS_WATCH_CONFIG=false in the service environment, disables auto-reload. Use harness reload, or kill -HUP the daemon.
  • The daemon reads a different file than the one you edited. The config row of harness doctor shows the path the daemon loaded. A HARNESS_CONFIG or --config in the service definition overrides the default.

harness says the daemon isn't running, but it is​

The client and the daemon disagree about the socket path. That usually means your shell and the service see different XDG_* variables, most often on macOS. Pin HARNESS_SOCKET to the same short path in both (details).

The daemon exits at startup with bind: invalid argument​

The socket path is too long: about 104 bytes on macOS, 108 on Linux. It happens when XDG_RUNTIME_DIR or XDG_STATE_HOME points somewhere deeply nested. Set HARNESS_SOCKET (or --socket) to a short path, such as ~/.local/state/harness/harness.sock.

Still stuck?​

Collect harness doctor, harness describe NAME, harness logs NAME, and the daemon's own log from around the time of the problem. Read --raw logs for secrets before sharing them; see the durable log.