Skip to content

Troubleshooting

William Koch edited this page Sep 30, 2026 · 17 revisions

Start with keryx doctor:

uv run keryx doctor        # --json for machine-readable output

It checks the configuration location, the migration status, the OpenAI key, the coding-agent sign-ins, the Twilio settings and signature validation, PUBLIC_HOST, the tunnel software, the caller allowlist, the PIN, the service manager, and whether DATA_DIR is writable. It also checks that your secrets are readable only by you, stored outside a Git working tree, and absent from config.toml. The --fix option only tightens the permissions of secret files.

Further checks cover your name, the memory, PROJECTS_ROOT, Google, and each of the four plugins (send_to_slack, check_email, check_billing, and cluster_stats). For each plugin, it says whether it's on, off, or on but refused, and why. It also names any settings left in config.toml from before the plugins existed. For a feature that's only partly configured, doctor says what's missing.

The symbols mean:

  • ✅ — fine.
  • ○ — optional, and not set up.
  • ⚠️ — limits what Keryx can do.
  • ❌ — won't work. Only ❌ makes the command exit non-zero.

It's safe to run before anything is configured, and it never prints a secret. keryx setup reads the same checks to decide what to ask you.

Where to look

Thing Where
Server log STATE_DIR/logs/keryx.log (rotated, 10 MB × 5)
Service stdout/stderr STATE_DIR/logs/{keryx,cloudflared,ngrok}.{out,err}.log
The restart watchdog STATE_DIR/logs/restart-watch.log
Voice transcripts DATA_DIR/calls/<session_id>.log
Task progress DATA_DIR/tasks/<id>.log
Written reports DATA_DIR/tasks/<id>.md
Task rows DATA_DIR/tasks.db (or just keryx tasks show <id>)
Plugins and your own tools DATA_DIR/tools/ (or keryx plugins and keryx tools)
The last restart uv run keryx restart --status

Unless you've moved them, STATE_DIR is ~/.local/state/keryx and DATA_DIR is ~/.local/share/keryx. keryx config path prints every directory. See Data and Retention.

On Linux, journalctl --user -u keryx also shows the unit's own view.

Common cases

  • The call connects but nobody speaks — the media socket was rejected. Check that the caller is in ALLOWED_CALLERS, and that the webhook URL matches PUBLIC_HOST exactly.

  • Twilio shows 403 — signature validation failed. The tunnel host and the configured webhook URL don't match.

  • Tasks fail instantly — the coding agent isn't signed in, or its SDK isn't installed. keryx auth status and keryx doctor say which. To sign in, run keryx auth login claude (or codex).

  • A task with no project ran in DATA_DIR/workspace, not among your repositories — PROJECTS_ROOT doesn't exist on this machine. It defaults to ~/projects, and Keryx never creates it. Set it to where your code lives. keryx doctor says which directory it found.

  • A task under the service can't find a tool that works in your terminal (npx, conda, cargo…) — the service runs with the PATH of the shell you installed it from. From a shell that has the tool, run scripts/install-systemd.sh or scripts/install-launchd.sh again. See Running as a Service.

  • No text ever arrives — texting is off by default. If your Twilio account can send SMS to your number, run keryx config set SMS_ENABLED true. Calls work either way.

  • Keryx calls you "the owner", or talks as if it has never met you — OWNER_NAME is blank, or there's no memory yet. The About you section of keryx setup sets both. You can also run keryx config set OWNER_NAME … and keryx memory seed; see Command Line Reference.

  • A project's brief never makes it into a call — briefs are capped at 1,500 characters each and 6,000 in total. A brief that would go past the total is left out entirely, and the log names it: left N project brief(s) out of the prompt, past 6000 characters in all: …. Shorten the briefs. Projects listed in PROJECTS are included first.

  • Slack, email, billing, or cluster stats is never offered — these four are plugins, and none is offered until you turn it on. Use keryx setup → Plugins, or keryx plugins install NAME. keryx plugins shows which are on. Each call reads the plugins again, so turning one on or editing its .toml doesn't need a restart.

  • keryx plugins says a plugin is refused — its files are there, but the next call wouldn't load it. The line under it says why:

    • no bot token and channel (send_to_slack) — run keryx config set SLACK_BOT_TOKEN --stdin and set channel_id in send_to_slack.toml. Alternatively, set an mcp_server whose config has both.
    • not signed in to Gmail or the claude CLI is not installed (check_email) — run keryx auth login gmail, or uv sync --extra claude.
    • [clusters] in cluster_stats.toml names no host, or the guard … is not a file — add a host (keryx plugins hosts lists the ones it could ask), or blank guard to use the built-in guard.
    • does not parse, or a value that doesn't validate — fix the .toml by hand, or walk keryx setup → Plugins again, which rewrites it.
    • writable by other users (chmod go-w) or owned by another user — someone else could have written the file, or DATA_DIR/tools itself, so Keryx refuses it without reading it. Make it yours and run chmod go-w on it.

    When a plugin it just turned on is refused, keryx plugins install says the same and exits 1. It never takes a secret on its command line. Instead, it points you to keryx config set KEY --stdin or keryx auth login gmail.

  • keryx plugins install says only the owner may install a plugin — it ran inside a Keryx task. Only you, at your own terminal, can install or remove plugins.

  • cluster_stats says the login has expired — it only asks over an ssh ControlMaster that you already have open, and never opens a connection of its own. To bring the master up, log in to that host in a terminal. keryx plugins hosts shows which masters are up now.

  • A tool of your own is never offered — keryx tools lists what DATA_DIR/tools holds, and why a file is refused. A file is refused if it won't import, defines no tool, takes a built-in tool's name, or is writable by someone else. The command exits 1 while any file is refused. A file whose name starts with _ is skipped on purpose. See Custom Tools.

  • A setting you changed has no effect — keryx config list shows where each value comes from. The process environment, such as a unit's Environment=, wins over the store.

  • Work is refused on the phone — you haven't given the PIN on this call. Before the PIN, a call can hear what Keryx knows, but can't act on it. See Security Model.

  • A call opens without telling you what finished — either there's no PIN yet, or BRIEFING_BEFORE_PIN is off. Until a PIN exists, nothing of yours is read out: set one in keryx setup, or key one in on the first call. With BRIEFING_BEFORE_PIN off, the results follow once you give the PIN.

  • "PIN entry is locked for now", and then it hangs up — wrong PINs across calls reached PIN_FAILURE_LIMIT (10 in 24 hours by default). PIN entry is locked for PIN_LOCKOUT_MINUTES (an hour), and until then the right PIN is refused too. If this wasn't you, someone is guessing: change the PIN in keryx setup. To clear the lock early, stop Keryx, delete DATA_DIR/pin-failures.json, and start Keryx again. The log line to look for is PIN entry is locked on every call until ….

  • "Sorry, this line is busy" — MAX_PHONE_SESSIONS calls (two by default) were already in progress, or had just been answered. A dropped call can take a few seconds to close. Wait a moment and call again.

  • keryx serve says DEBUG_SKIP_TWILIO_VALIDATION is on while PUBLIC_HOST is set — that switch is only for a machine that nothing outside can reach, so it can't be combined with a tunnel. Turn it off: keryx config set DEBUG_SKIP_TWILIO_VALIDATION false.

  • The approval bridge never rings — check uv run keryx approvals first. If a prompt you left waiting doesn't appear there as raised, Keryx decided it wasn't eligible. The server log says why, on a line starting not escalating a … prompt:.

    • the approval hook that sent it does not say whether it trimmed the request — the hook in ~/.claude/hooks/ is older than Keryx. Run scripts/install-claude-hook.sh again.
    • the hook had to trim it — the command, or the content of the file write, is longer than 4096 characters. This is deliberate, and the prompt waits on screen.
    • is not an option a keypad may approve, is not the name of a remote, the command is not on the shell allowlist, means something to the shell, cannot be said whole, it writes into .git — all deliberate. See The Approval Bridge for what the policy allows, and why.
  • Google tools fail in a task — the stored credentials may have expired. Run keryx auth login google-workspace again. This applies only when GOOGLE_WORKSPACE_MCP is on; see Google Workspace MCP.

  • Keryx cuts you off while you're thinking — that's turn detection, not a fault. VAD_EAGERNESS=low is more patient than the default, medium. To go back to a fixed timer that you can tune directly, set VAD_MODE=server and VAD_SILENCE_MS.

  • "Restart yourself" refuses, or keryx restart says there's no installed service — nothing supervises the process that was asked. A keryx serve started in a terminal can't restart itself, even where the service is installed too. keryx doctor says which unit it found. See Running as a Service.

  • A restart "failed" with exit code -15 — it didn't. systemctl runs inside the service it restarts, so the restart stops it too. See Restarting Keryx.

  • The service never came back, and nothing told you — check systemctl --user status keryx and STATE_DIR/logs/keryx.log. uv run keryx restart --status shows what the logs said at the time.

Related

Clone this wiki locally