Troubleshooting
Predictable failure modes. Fix command first; explanation second.
Predictable failure modes. Fix command first; explanation second.
If a behaviour isn't listed here, start with:
jun doctor # green/red checklist with fix commands
jun logs -f # follow logs/agent.log
jun config show # see the resolved config (secrets masked)
jun status # is the orchestrator running? listening where?jun doctor is the first call. It checks the Linear key, the team, the workflow states, the labels, every runner binary, the webhook registration, and pushes a signed fake webhook through the in-process orchestrator. The failure rows print the exact fix command.
Issue is stuck in Backlog and nothing happens
Fix: move the issue out of Backlog (e.g. to Todo) and post a comment.
Why: Backlog is hard-ignored. june never engages with an issue in Backlog — no DB row, no comment-thread mapping, no run, even on a fresh comment. The state is deliberately inert so unfiled drafts don't get a Claude session every time someone comments on them. See concepts.md → State machine.
If an issue you care about moved to Backlog while a run was active, that run was killed at the transition (moved to Backlog by user).
Webhook fires but I never see a run
Fix: jun doctor and look at the Webhook registered and Local smoke rows. Most common causes:
- Signature mismatch.
jun logs -fshowsinvalid signature401s. Check thatLINEAR_WEBHOOK_SECRETin your env matches the secret on the Linear webhook config. If you rotated one and not the other, re-runningjun setupwill pick a fresh URL/secret and re-register. - Wrong URL. Linear is delivering to a stale public URL (the tunnel rotated, ngrok restarted). Restart the tunnel, then re-run
jun setupand paste the new URL — it'll update the registered webhook in place. - Team mismatch. The webhook is scoped to a team, and the issue isn't in that team. Linear delivers nothing in that case.
jun config showwill print the resolvedteamKey. - Webhook disabled in Linear. Doctor's
Webhook registeredrow will saydisabled. Toggle it in Linear's webhook settings.
Webhook delivers but the issue moves to Blocked with runner CLI unavailable
Fix: install the runner CLI for the label on the issue (claude, codex, opencode, agy, or hermes), and restart june (jun stop && jun start, or for a service: jun uninstall-service && jun install-service). jun setup and jun doctor both report which runners they found on $PATH.
Why: june probes each runner binary at boot. A missing CLI gets a null runner; any Linear label that would route to it falls back to Claude, with a redirect comment on the issue. If Claude itself is missing, june refuses to boot — that's the one runner that must be installed.
If the binary is installed but in a non-default location:
JUNE_CLAUDE_BIN=/Users/you/.bun/bin/claude
JUNE_CODEX_BIN=/opt/homebrew/opt/codex/bin/codex
JUNE_OPENCODE_BIN=...
JUNE_ANTIGRAVITY_BIN=...
JUNE_HERMES_BIN=...This is especially common under launchd / systemd, which strip PATH down to /usr/bin:/bin and lose ~/.local/bin and /opt/homebrew/bin along the way.
Runner says "not authenticated" or fails immediately with empty output
Fix: log in on the host as the user june runs as:
| Runner | Command | State dir |
|---|---|---|
| Claude | claude (interactive once) | ~/.claude/ |
| Codex | codex login | ~/.codex/ |
| OpenCode | opencode auth login | ~/.opencode/ |
| Antigravity | Sign into the desktop app | desktop-managed |
| Hermes | hermes init then configure a provider | ~/.hermes/ |
Pay attention to $HOME: if june runs under launchd as your normal user, it uses your normal $HOME. If it runs under a dedicated _june user, that user needs its own login session for each runner.
The Claude runner deletes ANTHROPIC_API_KEY from the child env before spawning, so even if you set the API key, Claude will look for the logged-in user account. This is on purpose; see security.md → How runner auth works.
The orchestrator started but isn't moving the issue's state
Fix: re-run jun setup (it ensures the missing states and labels are present, idempotent), or call the targeted scripts directly: npm run ensure-states && npm run ensure-labels. Or check that your team has Ready for Review and Blocked (or whatever you set JUNE_REVIEW_STATES / JUNE_BLOCKED_STATES to).
Why: When state lookup returns nothing, the orchestrator logs getStateMap failed (or waiting_state_missing) and skips the move. The run still completes; the comment is still posted; the state just stays where it was.
If your team uses different state names than the defaults (In Progress, Ready for Review, Blocked), the right knob is JUNE_WORKING_STATES / JUNE_REVIEW_STATES / JUNE_BLOCKED_STATES, not renaming the states. See configuration.md → States.
The agent is replying to itself in a loop
Fix: add the bot's Linear user email to JUNE_BOT_USER_EMAILS (comma-separated). The viewer email of your LINEAR_API_KEY is added automatically at boot, so this only kicks in when you have multiple Linear bots in the workspace.
Why: june filters out the bot's own comments before passing them to the agent and ignores incoming comment webhooks whose author email matches the bot's. The filter is keyed on email; an additional bot account needs to be declared.
Cloudflare Tunnel URL changed and Linear is still pointing at the old one
Fix: restart the tunnel, then re-run jun setup — it finds the existing june-labelled webhook for your team and offers to update its URL in place. Idempotent.
For a permanent URL, set up a named tunnel: cloudflared tunnel login, cloudflared tunnel create june, cloudflared tunnel route dns june june.<your-domain>. Docs: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/create-local-tunnel/.
Run is taking forever and I want to stop it
Fix:
curl -X POST -H "Authorization: Bearer $JUNE_ADMIN_TOKEN" \
http://127.0.0.1:8787/admin/runs/<run-id>/stopFind the <run-id> from logs/agent.log (look for starting agent run / runId) or from the SQLite DB at data/agent.sqlite:
sqlite3 data/agent.sqlite "SELECT id, issue_id, status, started_at FROM runs WHERE status='running'"The endpoint SIGTERMs the run's process group, SIGKILLs it after 5 s, marks the row stopped, posts a Blocked. comment, and moves the issue to Blocked.
If JUNE_ADMIN_TOKEN isn't set, the endpoint responds 403 admin disabled. Set the token, restart, retry. Or just kill the orchestrator and let next-boot reconciliation handle it.
Orchestrator crashed mid-run; the next boot keeps the issue in In Progress
Fix: the orchestrator will reconcile on next boot — wait a few seconds, you'll see a Blocked. comment and the issue will move to Blocked. Or force it now:
curl -X POST -H "Authorization: Bearer $JUNE_ADMIN_TOKEN" \
http://127.0.0.1:8787/admin/reconcileWhy: Each runner is spawned detached: true. If the orchestrator dies, the child can survive. On next boot, june checks every DB row marked running:
- If the child pid is still alive, june watches it and reconciles when it exits.
- If the pid is dead, the row is marked orphaned immediately and reconciled.
Reconciliation = a Linear comment + a state move to Blocked. The issue is never silently abandoned.
Run keeps restarting on a tight loop
Fix: stop commenting on the issue. Each new comment during an active run steers the run — SIGTERM + restart with the new comment as input. If you're typing fast, you can get into a state where the orchestrator chains restart after restart.
The cap is JUNE_COMMENT_MAX_AUTO_FLUSHES (default 3). After hitting that cap, queued comments stay in the DB and the next routing webhook drains them. So just stop commenting for ~5 s and the storm subsides.
If you actually want to redirect mid-task and don't want the steer behaviour, set JUNE_COMMENT_DEBOUNCE_MS=0 (defeats the debounce side; steer behaviour is separate) — actually no: steer happens any time a comment arrives during an active run, period. The only knob is JUNE_COMMENT_MAX_AUTO_FLUSHES.
Linear shows Health check at 25 min → TAKEOVER and the run restarted
Fix: that's the watchdog working as designed. The run wasn't writing to its session log; june killed it and queued a fresh start with a "previous run was hung" directive. If this is happening too aggressively, raise JUNE_CLAUDE_WATCHDOG_HARD_IDLE_MINUTES (default 25).
Why: the watchdog Claude makes a verdict every JUNE_CLAUDE_HEALTH_CHECK_INTERVAL_MS (default 15 min). The hard-idle takeover (silent for 25+ min) skips spending a watchdog Claude and just kills. See concepts.md → Health watchdog. Disable entirely with JUNE_CLAUDE_HEALTH_CHECK_INTERVAL_MS=0.
SQLite "database is locked" in logs
Fix: rare. Usually means two node processes are pointed at the same data/agent.sqlite. jun status reports a stale pid file if one is lingering; otherwise ps aux | grep server.mjs and kill the duplicate. Running both jun start and jun install-service's background unit at the same time will race — pick one.
june opens the DB with journal_mode=WAL so concurrent readers are fine; the lock is on writes only.
Issue not getting marked Waiting even though it's been sitting in Ready for Review for hours
Fix: check that JUNE_WAITING_SWEEP_ENABLED=1 (the default — confirm with jun config show) and that your team has a Waiting state (or whatever you set JUNE_WAITING_STATES[0] to). jun doctor does not currently exercise the sweep — easiest verify is to jun logs -f and look for waiting sweep moved issue lines.
Why: sweep ticks at JUNE_WAITING_SWEEP_INTERVAL_MS (default 5 min) and moves issues whose latest comment is from the bot and is older than JUNE_WAITING_AFTER_MS (default 1 h). Common reasons the sweep skips an issue:
- the latest comment is from a human (good — the human is still engaged);
- the latest comment is from the bot but too recent (wait longer);
- the issue has an active run or a start pending;
- no
Waitingstate in the team (sweep no-ops withwaiting_state_missing).
JUNE_AGENT_ROOT change isn't picked up by sessions
Fix: move the issue out of an active state (e.g. to Backlog and back), or just accept that switching roots starts fresh sessions.
Why: june stores the working directory alongside each session id. If the stored root doesn't match the current JUNE_AGENT_ROOT, the session is dropped and a fresh one starts. This is intentional — a Claude session bound to /Users/old/workspace/ doesn't make sense when the orchestrator is now running in /Users/new/workspace/.
Comment never appears even though the run succeeded
Fix: jun logs -f for commentCreate errors. If Linear is returning a 403 / 401, the API key has been revoked or scoped down (re-run jun setup to rotate it). If it's returning a 400, the comment body might be too large; june doesn't currently truncate posted comments (only the prompt input is truncated, at 25 000 chars).
The final stdout is the deliverable; if it's empty or starts with BLOCKED:, the comment will say Blocked.\n\nfinal answer is empty instead. That's the verification gate doing its job.
Doctor passes but real webhooks still fail
Fix: the doctor smoke test bypasses the runner CLI and uses a FakeClaude. It exercises HMAC verification, the orchestrator's state machine, comment posting, and state transitions, but it does not validate that the real Claude / Codex / etc. binary is callable end-to-end. Run npm run acceptance:live for a true end-to-end probe — it creates a disposable Linear issue and waits for it to reach Ready for Review.
If acceptance:live hangs in In Progress, the runner isn't producing output. jun logs -f and look for final answer is empty or Claude exited <code>.
I can't find JUNE_LINEAR_API_KEY in the config
It's LINEAR_API_KEY (no JUNE_ prefix). Same for LINEAR_WEBHOOK_SECRET. The JUNE_*-prefixed env vars are june-specific tunables; the Linear credentials reuse Linear's own naming.
Where to look for more
jun logs -f(or<appDir>/logs/agent.log) — every webhook, every run, every state transition.<appDir>/data/agent.sqlite— open withsqlite3and queryruns,linear_issues,queued_messages,linear_events.jun config showprintsdatabasePath.~/.claude/projects/<slug>/<sessionId>.jsonl— Claude's per-session event log (what the watchdog reads).~/.opencode/,~/.codex/,~/.hermes/— analogous state dirs for the other runners.npm run phase0— sanity-checks the Linear API contracts and the exact non-interactive Claude command. Useful when something feels broken at the GraphQL level.
If you've gotten this far and you still don't know why something is wrong, the codebase is small enough to read end to end:
src/server.mjs— HTTP + boot.src/orchestrator.mjs— webhook handling, steering, watchdog, sweep.src/config.mjs— every env var.src/<runner>-runner.mjs— one file per runner, each ~100–200 lines.