Runners
Runner reference
CLI commands, runner.json keys, state files and logs.
Everything about agentdock-runner in one place: its commands and exit codes, every key of runner.json, the files it keeps, its environment variables and logs, the OTLP receiver, and the commands the server can send it. The facts here are from runner 0.1.0.
Commands
Usage: agentdock-runner <command>
Commands:
pair --server <url> --code <XXXX-XXXX> [--no-detect]
run
status
profiles [--detect [--write]]
install-service
version| Command | What it does |
|---|---|
pair --server <api-origin> --code XXXX-XXXX | Exchanges a one-time pairing code for a runner id and token and writes them to runner.json. If the config has no profiles yet, it also detects them and prints each with its authentication state. Pairing again replaces the previous pairing; existing profiles are kept. |
pair ... --no-detect | Pairs without detecting profiles. |
run | The daemon, in the foreground. Refuses to start when the machine is not paired. Warns when no profiles are configured. |
status | Prints the config path, whether it is paired and as which id, the server, the number of profiles, the spool state, and whether the server answers. Exits 1 when the server is unreachable. |
profiles | Prints the configured profiles as JSON, each with authenticated. |
profiles --detect | Prints the profiles detection would propose. Changes nothing. |
profiles --detect --write | Replaces the configured profiles with the proposal. --write without --detect is a usage error. |
install-service | Writes a systemd user unit (Linux) or a launchd agent (macOS) and prints the command that starts it. |
version (also --version, -v) | Prints the version and the target, for example 0.1.0 darwin-arm64. |
help (also --help, -h) | Prints the usage above. |
--server is the API origin: http(s)://host[:port][/path], without credentials, a query or a fragment. The runner posts to <server>/runners/pair and connects to <server>/runner (ws:// for http://, wss:// for https://). Behind the reference proxy that is https://<host>/api.
There is one more subcommand, exec-run <runDir>, which a skill run's tmux session executes. It is internal; do not call it yourself.
Exit codes
| Code | Meaning | Service behaviour |
|---|---|---|
0 | Success, or a clean stop | not restarted |
1 | Failure: crash, unreachable server, invalid config, pairing error, lock held | systemd restarts it after 5 s; launchd and Homebrew after their throttle |
2 | Wrong arguments | |
78 | The server closed the connection for good: protocol mismatch (4400), revoked or invalid token (4401), or replaced by a newer pairing (4409) | not restarted: RestartPreventExitStatus=78 on systemd; a /bin/sh wrapper turns 78 into 0 for launchd and brew services |
While running, the runner reconnects with exponential backoff from 1 to 60 seconds on any other disconnect.
runner.json
The config lives at $XDG_CONFIG_HOME/agentdock/runner.json, by default ~/.config/agentdock/runner.json. The runner writes it atomically with mode 0600. It is read when the daemon starts, so restart the runner after you edit it. An invalid file is refused with the names of the bad fields (never their values, since one of them is the token).
{
"server": "https://agentdock.example.com/api",
"runnerId": "...",
"token": "...",
"profiles": [
{ "id": "claude-default", "runtime": "claude", "env": {}, "args": [] }
],
"projects": [],
"disabledCommands": ["terminal.attach"],
"otlp": { "enabled": true, "http": 4318, "codexExperimental": false },
"fleet": { "pollSeconds": 15, "prPollSeconds": 60, "eventsPollSeconds": 5 },
"queue": { "pollSeconds": 60 },
"sessions": { "enabled": true, "ingestSince": "2026-10-13T09:00:00.000Z" },
"skills": { "maxConcurrentRuns": 2, "maxTimeoutSec": 21600 }
}| Key | Default | Meaning |
|---|---|---|
server | none | API origin, written by pair. |
runnerId | none | Written by pair. |
token | none | Written by pair. Redacted from logs. Treat the file as a secret. |
profiles | [] | Runtime profiles: id, runtime, optional binary, env, args. See Runtime profiles. |
projects | [] | The runner's cached watch list (id, root). The server manages it when projects are connected; do not edit it. |
disabledCommands | [] | Server commands this machine refuses. See Disabling commands. |
otlp | null | The OTLP receiver. null means on, on 127.0.0.1:4318. |
otlp.enabled | true | Start the receiver. |
otlp.http | 4318 | Port. The host is always 127.0.0.1. |
otlp.grpc | null | Reserved. gRPC is not served. |
otlp.codexExperimental | false | Also map codex.* telemetry records, whose attribute names are not verified yet. |
fleet.pollSeconds | 15 | Fleet collectors' poll interval (5 to 3600). |
fleet.prPollSeconds | 60 | Pull request and checks poll interval through gh (15 to 3600). |
fleet.eventsPollSeconds | 5 | Poll fallback next to file watching for Code Sentinel's events.jsonl (1 to 3600). |
queue.pollSeconds | 60 | How often the issues collector polls GitHub for a project's open issues (15 to 3600). |
sessions.enabled | true | Read agent transcripts and send session metadata. false turns session ingest off. |
sessions.ingestSince | set by pair | Transcripts last modified before this time are skipped unless an admin runs a backfill. |
skills.maxConcurrentRuns | 2 | Skill runs at once (1 to 32). The rest wait as queued, oldest first. |
skills.maxTimeoutSec | 21600 | A skill run's timeout above this is cut to it (60 to 21600). |
Files
State lives under $XDG_STATE_HOME/agentdock/, by default ~/.local/state/agentdock/.
spool/ events not yet acknowledged by the server
spool/runner.lock one daemon per machine
offsets.json how far each agent transcript was read
events-offsets.json byte offset and inode of each project's events.jsonl
runs/<runId>/ one directory per skill run
run.json what to launch, as an argv
state.json the run's state
stream.jsonl the session's output stream
stderr.log
exit.json
patch.diff the full patch (the server keeps at most 128 KiB)Events are delivered at least once: they sit in the spool until the server acknowledges them, so a runner that loses its connection catches up when it returns.
Service files:
| What | Where |
|---|---|
| systemd user unit (Linux) | $XDG_CONFIG_HOME/systemd/user/agentdock-runner.service |
| launchd agent (macOS) | ~/Library/LaunchAgents/com.agentdock.runner.plist |
Environment variables
| Variable | Effect |
|---|---|
AGENTDOCK_LOG | Log level: debug, info (default), warn or error. |
HOME | Base for every default path. |
XDG_CONFIG_HOME | Where agentdock/runner.json and the systemd unit go. Used only when it is an absolute path. |
XDG_STATE_HOME | Where the state directory goes. Used only when it is an absolute path. |
CODEX_HOME | The Codex config directory that detection puts into codex-default. |
Logs
Logs are JSON lines on stderr, with the token redacted.
| How it runs | Where the log goes |
|---|---|
Foreground (run) | the terminal |
| systemd user unit | the journal: journalctl --user -u agentdock-runner -f |
| launchd agent | ~/Library/Logs/agentdock-runner.log |
brew services | $(brew --prefix)/var/log/agentdock-runner.log |
Set AGENTDOCK_LOG=debug in the service's environment for more detail.
OTLP receiver
The runner listens for OpenTelemetry logs on 127.0.0.1:4318, so a Claude Code session with telemetry on reports each LLM request as it happens, before its transcript is read. Without it, tokens and cost still arrive from transcripts, just later.
- OTLP over HTTP only, on
POST /v1/logs, asapplication/x-protobuforapplication/json, optionally gzip./v1/metricsand/v1/tracesare accepted and discarded. - Connections from anything but loopback are refused, and so are bodies over 4 MiB.
- Only
claude_code.api_requestrecords are read. Each becomes one LLM request with model, request id, token counts, duration and the runtime's own reported cost. Prompts, tool input and output, responses, email and account or organization ids are never sent on, even withOTEL_LOG_USER_PROMPTS=1. - If the port is taken, the error is logged and the runner keeps running without the receiver.
Skill runs that AgentDock launches turn telemetry on by themselves. For any other Claude Code session on the machine:
CLAUDE_CODE_ENABLE_TELEMETRY=1 \
OTEL_LOGS_EXPORTER=otlp \
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf \
OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318 \
claudeYou can add OTEL_RESOURCE_ATTRIBUTES=agentdock.project=<id>,agentdock.slot=<slot>,agentdock.issue=<n>,agentdock.run=<run> to attribute requests directly. All four are optional; without them a request is matched to its session by session.id.
Command allowlist
The server can only ask a runner to do what is in this list. Each command has a schema for its arguments, a minimum role the requesting user must have on the project, and a timeout. There is no command that runs a shell string. Paths are confined to registered project roots and their .wt-<repo>-* sibling worktrees.
| Command | Minimum role | Timeout | What it does |
|---|---|---|---|
runner.ping | viewer | 30 s | Liveness check |
runner.describe | viewer | 30 s | Host facts and capabilities |
project.inspect | admin | 45 s | Read a repository before connecting it |
project.refresh | operator | 45 s | Re-read a connected project |
session.backfill | admin | 10 min | Re-read older transcripts |
orchestrator.start | operator | 30 s | Start the orchestrator in tmux |
orchestrator.stop | operator | 10 s | Kill the orchestrator's session only |
orchestrator.status | viewer | 5 s | Presence and pane state |
slot.stop | operator | 10 s | Kill a slot's tmux session, keep its worktree |
slot.message | operator | 10 s | Write .orchestrator-msg.md and nudge the worker |
issue.create | operator | 45 s | Create a GitHub issue with gh |
issues.refresh | operator | 45 s | Poll the queue now |
collector.poll | admin | 30 s | Poll collectors now (GitHub App push) |
pr.inspect | viewer | 30 s | Pull request details for approvals |
pr.approve | operator | 10 s | Write an approval signal file |
pr.requestChanges | operator | 10 s | Write a request-changes signal file |
pr.voidApproval | operator | 10 s | Withdraw a decision |
terminal.attach | admin | 10 s | Open a PTY on a tmux session |
skill.search | operator | 15 s | Search the skills.sh catalog |
skill.inspect | operator | 2 min | Clone and hash a skill for preview |
skill.install | operator | 3 min | Install a skill (profile installs need admin) |
skill.uninstall | admin | 10 s | Remove an installed skill |
skill.list | viewer | 60 s | Installed skills |
skill.run | operator | 30 s | Start a headless skill run |
skill.cancel | operator | 15 s | Cancel a skill run |
The timeout is how long the server waits for the command to answer, not how long the work may take: skill.run answers once the run is started.
Disabling commands
disabledCommands lets the owner of a machine refuse commands no matter what the server asks. A disabled command answers with the error disabled. For example, to forbid terminal attach and skill runs on this machine:
{
"disabledCommands": ["terminal.attach", "skill.run", "skill.install"]
}Restart the runner after the change. Disabling terminal.attach also turns off the runner's terminal capability, so the UI stops offering Attach for it.