Help us shape the future of Go! Take the 2026 Go Developer Survey

clankerbar-cli

module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2026 License: MIT

README

clankerbar

Drive a coding agent through your clankerbar backlog, unattended. clankerbar respawns fresh harness sessions that drain the backlog and survives usage limits — pausing on the cap and resuming (including on an early reset) instead of dying until you come back.

It's a local, open-source client of the clankerbar control plane: the hosted plane holds the state (your backlog, over MCP), this runs on your machine and drives your coding agent. Because it holds your credentials and can edit your code and run shell commands, the source is open so you can audit exactly what it does before you trust it with an overnight run.

Status: pre-1.0, released. The loop runs and is what drives clankerbar's own backlog; the harness adapters and the limit/budget machinery are still being hardened. It is a supported surface rather than an internal tool - see Versioning for what the v0.x line does and does not promise.

How it works

Each iteration is a fresh harness session told to work the backlog (which it does by dispatching tasks to subagents and landing PRs at in_review). When a session ends, the driver spawns a new one — so context never rots across a long run, and a session killed mid-task is fine: the driver hands the task straight back to the queue (see Resilience) and the next iteration picks it up. The backlog is the durable state; the loop is thin.

On a usage limit the loop doesn't die — it pauses and polls for the reset (catching Anthropic's semi-random early resets), then continues.

Install

Download a prebuilt binary. Each release ships cross-platform binaries on the releases page - macOS and Linux, amd64 and arm64, as clankerbar_<version>_<os>_<arch>.tar.gz. Extract it and put clankerbar on your PATH. No Go toolchain required.

# macOS arm64; swap the os/arch for yours.
VERSION=0.1.0 OSARCH=darwin_arm64
curl -fsSLO "https://github.com/lecstor/clankerbar-cli/releases/download/v${VERSION}/clankerbar_${VERSION}_${OSARCH}.tar.gz"
curl -fsSLO "https://github.com/lecstor/clankerbar-cli/releases/download/v${VERSION}/checksums.txt"
shasum -a 256 --ignore-missing -c checksums.txt   # Linux: sha256sum --ignore-missing -c checksums.txt
tar -xzf "clankerbar_${VERSION}_${OSARCH}.tar.gz"
./clankerbar version

Then verify where it came from, not just that it arrived intact. The checksum above catches a corrupt or truncated download, and that is all it catches: it is served from the same release page as the archive, so anyone able to replace one could replace both. Every release is also signed with build provenance - a Sigstore attestation held by GitHub, not by the release, recording that these exact bytes came out of this repo's release.yml at a specific commit:

gh attestation verify "clankerbar_${VERSION}_${OSARCH}.tar.gz" \
  --repo lecstor/clankerbar-cli \
  --signer-workflow lecstor/clankerbar-cli/.github/workflows/release.yml

--signer-workflow is the half that makes the sentence above true: --repo alone accepts an attestation signed by any workflow in this repo, while naming the workflow pins it to the one that publishes releases. That is worth the extra line - this binary holds your credentials and runs shell commands on your machine, and provenance is the part that would notice if the archive were not ours. Needs the GitHub CLI and a gh auth login; run gh attestation verify --help for the offline and JSON-output variants.

Downloading in a browser instead of with curl gets the archive quarantined by macOS, and the binaries are not notarized, so the first run is refused with "cannot be opened because the developer cannot be verified". Clear it with xattr -d com.apple.quarantine clankerbar, or use the curl route above, which never sets the attribute.

Or build from source (requires Go 1.26+):

go install github.com/lecstor/clankerbar-cli/cmd/clankerbar@latest
# or, from a checkout:
go build -o clankerbar ./cmd/clankerbar

A source build reports the version it was stamped with - go install leaves it at the 0.0.0-dev default, so clankerbar version naming a real vX.Y.Z means you are on a release archive.

Versioning

Releases are semver tags (vX.Y.Z) and the tool is pre-1.0, so expect the v0.x line while the surface settles. Concretely, while pre-1.0:

  • A breaking change can land on a minor bump (v0.1 -> v0.2). That is what the v0.x line means; pin a version if you are automating against it.
  • A breaking change is never silent. Permitted is not the same as unannounced: breaking commits carry a ! (feat(cli)!: ...) and the release config hoists them into a Breaking changes heading at the top of the notes, rather than leaving them as one line among thirty.
  • Releases are cut when there is something to ship, not on a schedule. The commitment is that a tag produces working, downloadable binaries.

Those promises harden at v1.0.

Usage

clankerbar run --harness=claude
clankerbar run --harness=claude --model=opus --max-iterations=10
clankerbar run --config ./clankerbar.json     # or: -c ./clankerbar.json

Flags are GNU-style: --long options, -x shorts. --config (-c) and --help (-h) are the only short aliases; everything else is long-form only. clankerbar run --help and clankerbar doctor --help list them. A short flag's value is separate (-c ./x.json) or =-joined (-c=./x.json); the inline -c./x.json form is rejected, so a typo like -cofnig cannot quietly become --config=ofnig.

If you built from a checkout before v0.1.0, this changed under you. A single dash now introduces short flags, so the Go-stdlib spelling -harness claude no longer works - use --harness claude. Anything invoking clankerbar with single-dash long options (a cron wrapper, a launchd plist, a shell alias) needs updating. The old spelling is rejected with a message naming the double-dash form rather than being quietly reinterpreted. v0.1.0 is the first release, so it ships the GNU-style spelling from the start.

Preflight: clankerbar doctor

Most of what goes wrong in an unattended run only shows up as degraded behaviour, hours in — a rejected key silently drops the loop into blind mode, a missing binary kills the first session, an unreachable plane looks exactly like an empty queue. doctor turns all of that into one cheap answer before you start:

clankerbar doctor
clankerbar doctor --config ./clankerbar.json --harness codex   # or: -c ./clankerbar.json
clankerbar doctor && clankerbar run --harness=claude   # gate a cron wrapper

It prints one PASS / WARN / FAIL line per check, with a one-line remedy under anything that isn't a PASS, and exits non-zero if any check FAILs — so it composes with && as above.

PASS  config       loaded ./clankerbar.json
                   harness: claude
                   workdir: /Users/you/dev
                   backlog: https://clankerbar.com/api/projects/acme/backlog-summary
PASS  harness      /usr/local/bin/claude (2.1.0)
WARN  config_dir   not set — the session inherits the ambient environment
                -> set config_dir (or --config-dir) so a cron/launchd run loads the same skills, plugins and auth as your terminal
WARN  backlog      https://clankerbar.com/api/projects/acme/backlog-summary — 0 claimable, 2 open question(s) — nothing to claim; the loop will idle without spawning
                -> answer the open question(s) at clankerbar.com, or expect an idle run
WARN  state_dir    /Users/you/dev/.clankerbar-loop has a leftover STOP marker
                -> delete it, or the loop stops immediately: rm /Users/you/dev/.clankerbar-loop/STOP
WARN  workdir[acme] /Users/you/dev has no agent-instructions file (AGENTS.md / CLAUDE.md)
                -> add one here naming each repo below and where its protocol lives — a session started in a multi-repo parent loads nothing from the repos under it
PASS  permissions  /Users/you/.config/clankerbar/headless.json parses
WARN  toolchains   no grant for: go (/Users/you/dev/acme-cli)
                -> allow the verbs each one needs in /Users/you/.config/clankerbar/headless.json (e.g. Bash(go build:*), Bash(go vet:*), Bash(go test:*)) — a headless session fails closed, so an ungranted tool is refused with no prompt and its task ships unverified
PASS  budget       no ceiling configured — the loop runs until the backlog is dry or it is stopped

The checks: config (discovered, parses, validates — plus the resolved harness, workdir and derived backlog URLs), harness (binary on PATH and runnable, with its version), config_dir (resolves, exists, looks initialised for the chosen harness), backlog (creds present and the summary read succeeds — distinguishing no creds, a rejected key, a project_required key/route mismatch, and an unreachable endpoint — plus whether the queue is gated on your open questions, or paused from the console), state_dir (the driver's own directory: writable, no leftover HALT/STOP), workdir (per project: it resolves, an .mcp.json reaches it, and it carries an agent-instructions file), permissions (harness-specific policy sanity), toolchains (the build tools the project's repos need are actually granted), power (whether the machine will stay awake long enough to do the work), and budget (ceilings parse and are sane).

A multi-project config gets one backlog check and one workdir check per project — one queue can be wired wrong while the others are fine. A project entry that omits workdir or mcp_config_path inherits the top-level one, and doctor resolves it exactly the way the loop does, so it never reports on a directory your sessions will not use.

The toolchains audit reads every settings file Claude merges — the --settings policy, the config dir's settings.json/settings.local.json, and each session workdir's .claude/settings.json/.claude/settings.local.json. A narrow deny (Bash(go run:*) alongside an allowed Bash(go test:*)) is reported as a hole in an otherwise-granted tool, not as a blocked one — only a bare Bash(go:*) deny means the toolchain is unusable.

Three of those exist because of failure modes that cost a real overnight run whole iterations:

  • The queue said there was work and there wasn't. A ready task gated on an unanswered question still counts as claimable, so the loop spawns, the session correctly declines to pre-empt your decision, and you pay for that report ten times. Preflight now says so before the window opens.
  • A session started in a multi-repo parent reads nothing. A harness loads its instruction file, skills and project settings from the session's cwd and upward — never from the repos below it. Spawn in ~/dev and every session begins by rediscovering the layout, with none of your conventions.
  • An ungranted toolchain does not stop a run, it un-verifies one. A headless session fails closed, so go test that was never allowed is refused with no prompt reaching you — and the task ships written, pushed and never compiled.
  • A sleeping machine does not pause a run, it freezes one. Timers use the monotonic clock, which does not advance while the machine is suspended, so a wait stops mid-flight and the loop goes silent exactly like a hang. One run lost 5h31m of a 10h window this way, waking only in 45-second Power Nap bursts.
Sleep, on laptops

clankerbar run now holds a no-idle-sleep assertion for its own lifetime (macOS; it dies with the process, so nothing outlives the run). Two caveats it cannot fix, both of which have cost a real run its night:

  • A closed lid sleeps regardless. No assertion overrides clamshell sleep.
  • Start the run on AC. Plugging in later does not wake a sleeping Mac, so a machine that idle-slept on battery stays down even once power arrives — its never-idle-sleep-on-AC setting never gets a chance to apply.

When a wait does get frozen, the loop now says so on the way out, rather than leaving an unexplained gap in the log:

wait of 30m0s took 2h28m11s of wall clock — the machine was suspended for ~1h58m11s;
timers are frozen while it sleeps, so an unattended run stalls silently
When the queue lies about having work

claimable > 0 means work is available, not that it can be done. A task gated on an unanswered question is claimable and unworkable: the loop spawns, the session correctly declines to pre-empt your decision, and you pay for that report every cycle. One run did it ten times.

So after three consecutive sessions that settle nothing — nothing reaching in_review or done, which is the only progress signal a backlog can see — the loop backs that project off for 15 minutes, then 30, then an hour, capped at two. Other projects keep draining. Any progress from anywhere, including you merging a PR, clears it immediately.

Three rather than one, because a genuinely large task can span several sessions before anything reaches a reviewer, and backing off then would throttle exactly the deep work the loop exists to do.

WARN vs FAIL is the "would this still make progress?" line: no creds and an unreachable plane WARN, because the loop drains blind and still gets work done; a rejected key and a key/route mismatch FAIL, because they never self-heal and every session the loop spawns carries the same broken credential.

Control an in-flight run with markers in the state dir (<workdir>/.clankerbar-loop):

touch .clankerbar-loop/STOP     # stop gracefully (responsive even mid-wait)

You can also pause a run remotely from the clankerbar web console (Settings → Pause) — per project. A paused project stops getting new sessions (other projects keep draining) and the loop idle-polls until you Resume — without exiting, and without killing an in-flight session (a pause is honoured between iterations, like STOP). Remote pause rides on the driver's cheap backlog read, so it needs a wired poller: a CLANKERBAR_API_KEY and a resolvable plane URL. Your account key (mint at clankerbar.com/account/api-keys) is the normal choice — the driver polls the project-in-path summary route whenever it knows the slug, which it derives from your .mcp.json's /mcp/<slug> URL or from the projects config (below). A project-scoped key works too (the CI-style setup). With no creds or no resolvable endpoint the loop drains blind and can't see the flag — the local STOP/HALT markers remain the fallback there. Two misconfigurations hard-stop (non-zero exit) instead of blind-draining doomed sessions: a revoked/wrong key (401/403), and an account key with no derivable project slug (400 project_required from the legacy slug-less route) — give the loop a slug via projects or an /mcp/<slug> .mcp.json.

Visibility

The driver logs milestones to your terminal as it goes (timestamped): spawning, queue state each poll, done + tokens/cost, usage-limit pauses, transient retries. The Claude harness runs with --output-format stream-json, so the agent's own progress — assistant text and → Tool markers — streams live too, and each attempt is captured to its own <state-dir>/iteration-<ts>.log.

Skills, plugins, and auth

A headless session loads project skills from the working directory (.claude/skills/) and user/plugin skills + auth from the config dir. Set config_dir (→ CLAUDE_CONFIG_DIR / CODEX_HOME) so an unattended run loads the same skills, plugins, and login as your interactive session — a bare cron environment would otherwise have none of them.

Config

Flags override the config file, which overrides defaults. Default file locations: ./clankerbar.json, then ~/.config/clankerbar/config.json. (JSON today; TOML is the likely final format.)

{
  "harness": "claude",
  "model": "opus",
  "prompt": "Work the backlog.",
  "mcp_config_path": "./.mcp.json",
  "config_dir": "~/.claude",
  "idle_poll_interval": "60s",
  "poll_interval": "30m",
  "max_retries": 0,
  "retry_cap": "5m",
  "budget": {
    "max_tokens": 0,
    "max_cost_usd": 0,
    "max_wall_clock": "6h"
  }
}

mcp_config_path defaults to <workdir>/.mcp.json when that file exists — Claude's headless mode does not auto-discover it, so the default is what gives spawned sessions their clankerbar tools (and gives the driver a project slug to poll with).

Point it at your workdir, not at a checkout of this repo. The .mcp.json at the root here is the maintainers' own agent wiring and names the clankerbar project slug; running the loop from inside this checkout would have it poll a queue you cannot read, which it refuses rather than drains.

Multi-project: one instance, many queues

One loop instance can drive several projects with a single account-scoped key — you never need an instance per project, or per-project keys. Declare the projects; the driver polls each queue and round-robins sessions across whichever have claimable work, each session spawning in its own project's workdir (whose .mcp.json names /mcp/<slug>, selecting that project's tools):

{
  "harness": "claude",
  "projects": [
    { "slug": "clankerbar", "workdir": "~/dev" },
    { "slug": "ezyapp", "workdir": "~/work/ezyapp" }
  ]
}

Per entry: slug (required, the <slug> in /mcp/<slug>), workdir, and optionally mcp_config_path (defaults to <workdir>/.mcp.json). Budgets, max_iterations, and the STOP/HALT markers stay instance-global — one operator, one spend pool; the console pause is per project. With no projects list, the top-level fields drive a single project exactly as before.

Resilience

A fresh session can die on a server blip (API 5xx / overloaded) or a network hiccup — nothing to do with the task. These are retried with exponential backoff (30s → 60s → … capped at retry_cap), re-running the same iteration; a fresh session reclaims any half-done task, so a retry costs minutes, not work. Detection is anchored (Claude's API Error: prefix, connection-error strings), so a task log that merely mentions an HTTP 500 isn't mistaken for a dead session, and a 400 still stops. max_retries: 0 (the default) means never give up — keep retrying at the ceiling until the API recovers, right for a daemon; set a positive number to bound it. A usage-limit pause and a transient retry both re-run the same iteration and neither advances the iteration count. STOP stays responsive during any wait.

Handing the task back. A claim carries a 30-minute lease that the session heartbeats. When a session ends mid-task, that lease is left ticking with nobody renewing it — and clankerbar charges the task a reclaim to sweep it up, of which there are only two before the task is parked for a human. So the driver watches the session's stream for what it claimed, and hands the task straight back (update_task(status: ready)) on every exit: usage limit, transient blip, outright failure, or a clean finish with the work unfinished. The usage-limit case is why this matters most — the pause that follows can be hours, and the release is ordered before it.

One case is deliberately left to expire: a task whose session pushed a branch. clankerbar computes its takeover hint only for an in-progress task with a dead lease, so handing that one back would discard the very flag telling the next clanker there is work to pick up. There it takes the reclaim and keeps the hand-off. Releasing is best-effort throughout — if the plane refuses or is unreachable, the loop logs it and carries on, since an expiring lease is merely the behaviour it already had.

Not getting locked out

No coding-agent harness lets a headless caller read its remaining subscription quota, so clankerbar cannot stop at a precise "80% of the window". Two reliable alternatives, in order of preference:

  1. Separate credentials — run the loop under a different account / API key than your interactive agent use, so it physically cannot spend your daytime quota. This is the only can't-be-wrong option.

  2. Budget circuit breaker — set budget.max_tokens / max_cost_usd / max_wall_clock; the loop self-accounts and stops early. Blunt but simple — tune it by watching a couple of runs.

    Prefer max_cost_usd. It comes straight from the harness's own total_cost_usd, so it measures the thing you actually care about. max_wall_clock counts the hours a run spends waiting out a usage limit, in which nothing is billed at all — one run elapsed 10h23m against an 8h ceiling with 5h31m of that asleep. Keep it as an outer bound on how late a run may finish, not as your spend ceiling. doctor warns if it is your only dial.

    max_tokens counts about ten times more than it used to. Token totals now include cache reads and writes, which input_tokens excludes and which dominate a long agentic session — one run reported 140,387 tokens against $147.98 of real spend, about $1.05 per thousand, which is no model's price. The old number was the wrong one, but a max_tokens you tuned against it will now trip roughly ten times sooner. Re-tune it, or move to max_cost_usd.

    max_wall_clock is measured on the monotonic clock, so time the machine spends suspended does not count against it. That is also the clock the limit-reset check uses, so the two cannot disagree about how much of a run is left.

    When a limit's stated reset falls beyond max_wall_clock, the loop stops right then and tells you when the quota returns, rather than sleeping through the window to run one session against the fresh quota and stop on the next check.

Harnesses

Harness Status
Claude Code (claude) primary
Codex (codex) adapter present; parsing being hardened

New harnesses are a small adapter (internal/harness). Contributions welcome.

License

MIT (see LICENSE).

Directories

Path Synopsis
cmd
clankerbar command
Command clankerbar drives a coding-agent CLI (Claude Code, Codex, ...) through a clankerbar backlog, unattended: it respawns fresh harness sessions that drain the backlog and survives usage limits.
Command clankerbar drives a coding-agent CLI (Claude Code, Codex, ...) through a clankerbar backlog, unattended: it respawns fresh harness sessions that drain the backlog and survives usage limits.
internal
backlog
Package backlog is the driver's own cheap, read-only view of the clankerbar backlog — used to decide whether there is claimable work before spending tokens on a harness session, to keep polling (and logging) while idle so the loop reacts when questions are answered, items are promoted, or new work is filed, and to learn when the operator has paused the run from the web console.
Package backlog is the driver's own cheap, read-only view of the clankerbar backlog — used to decide whether there is claimable work before spending tokens on a harness session, to keep polling (and logging) while idle so the loop reacts when questions are answered, items are promoted, or new work is filed, and to learn when the operator has paused the run from the web console.
cli
Package cli wires the command-line surface to the loop driver.
Package cli wires the command-line surface to the loop driver.
config
Package config holds the loop's runtime configuration: a file (JSON for now — TOML is the likely final format, matching Codex's own config) overlaid with explicit command-line flags.
Package config holds the loop's runtime configuration: a file (JSON for now — TOML is the likely final format, matching Codex's own config) overlaid with explicit command-line flags.
harness
Package harness abstracts a coding-agent CLI (Claude Code, Codex, ...) behind the small contract the loop needs.
Package harness abstracts a coding-agent CLI (Claude Code, Codex, ...) behind the small contract the loop needs.
loop
Package loop is the driver: a long-running daemon that gates each iteration on a cheap backlog read, spends a fresh harness session only when there is claimable work, and — crucially — stays alive and keeps polling when the queue is empty, so it reacts when questions are answered, items are promoted, or new work is filed.
Package loop is the driver: a long-running daemon that gates each iteration on a cheap backlog read, spends a fresh harness session only when there is claimable work, and — crucially — stays alive and keeps polling when the queue is empty, so it reacts when questions are answered, items are promoted, or new work is filed.
plane
Package plane is the driver's small WRITE client for the clankerbar control plane.
Package plane is the driver's small WRITE client for the clankerbar control plane.
secureurl
Package secureurl holds one rule, in one place: where a bearer token may be sent.
Package secureurl holds one rule, in one place: where a bearer token may be sent.
statedir
Package statedir owns the one directory the DAEMON writes on its own account: the loop's control markers (STOP/HALT) and its per-iteration transcripts.
Package statedir owns the one directory the DAEMON writes on its own account: the loop's control markers (STOP/HALT) and its per-iteration transcripts.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL