@alizain/doublecheck
v2.4.1
Published
Run self-authored LLM code-inspectors against a project — one sandboxed microVM agent per check, one markdown report each
Maintainers
Readme
doublecheck
Runs self-authored LLM code-inspectors ("checks") against a project — one sandboxed agent per check, in parallel — and writes one markdown report per check.
A check is a markdown file: a timeless standard — no frontmatter, no
schema; the body is the inspector's instructions. Checks come from one or
more --checks-dir directories (default: $TARGET/.agents/checks), so a
shared check set can serve a whole folder of repos. Everything run-specific —
the intent of the work under review, known nuances, sanctioned exceptions,
scope ("these changed files vs main") — arrives per run via --context; the
harness computes no diffs and knows nothing about git. A report is a
markdown file: whatever the agent writes, no imposed structure — and the
operator's agent reads every line of it, which the prompt tells the inspector.
Why this exists
Linters and type checkers catch what can be pattern-matched. The defects that survive them are judgment calls: a silent fallback that swallows a config error, an abstraction at the wrong layer, a "for now" that will outlive its author. doublecheck encodes your judgment about those — each check is a standard you actually hold, written as prose instructions to an inspector agent that can read the whole tree and reason about it.
The division of labor is deliberate: everything that requires judgment lives in the check body (what to flag, what to leave alone, where to look); everything mechanical lives in the harness (sandboxing, parallelism, report collection). The harness knows nothing about git, diffs, or languages, so it never needs to change when your standards do.
The loop
doublecheck minedistills your Claude Code history into durable preference observations (~/.doublecheck/catalog) — evidence of standards you have actually enforced in past conversations.- A human and/or agent authors checks from those observations. This step is deliberately unproductized: going from "observed preference" to "runnable standard" is judgment work.
doublecheck checkruns every check against a project, one sandboxed agent per check, and writes one report per check. Read the reports, fix what is real, tighten any check that cried wolf.
How it works
Per check: a scratch workdir gets prompt.txt (environment preamble +
operator run context, when given + check body + report contract). A microsandbox
microVM boots from a locally built image with the project bind-mounted
read-only at its real host path and the scratch dir mounted rw as the
guest cwd. The selected agent CLI (--agent: headless claude -p, or
codex exec) runs inside with its full toolkit and no permission gates —
the microVM is the safety boundary, the ro mount is the "don't touch
my repo" guarantee. The agent writes report.md; the host copies it to
$OUTPUT/<run-timestamp>/<check>.md (with --save-jsonl, alongside
<check>.stream.jsonl — the inspector's raw stream, kept so a run can be
audited after the guest is gone).
The project is never copied — every guest shares the live host directory (dirty files included) through the ro mount, so per-check cost is one temp dir and ~2 GiB of guest memory.
Exit 0 only when every check produced a report. An agent failure (exit ≠ 0, no report) writes a failure-record report and flips the run's exit code; missing token / checks / image abort loudly up front.
Setup
npm install -g @alizain/doublecheck # installs the `doublecheck` command; or, from a clone: pnpm installGuests boot from an image with both agent CLIs baked in, and no install
needs an image step. An installed release resolves
ghcr.io/alizain/doublecheck-guest:<its own version>; a clone resolves the
nearest release tag reachable from its checkout (git describe), so git
pull bringing a new tag is also the image update. Either way the runner
pulls the image on first use (~1 GB, once per version — the release workflow
publishes it for amd64+arm64 alongside the npm package).
The locally built image is only for iterating on Dockerfile.guest itself
(needs a running Docker daemon), opted into explicitly:
./scripts/build-guest-image.sh
DOUBLECHECK_GUEST_IMAGE=doublecheck-guest:latest pnpm doublecheck check …DOUBLECHECK_GUEST_IMAGE=<ref> works for any install — the named ref must
already be in the microsandbox cache (it is never pulled); useful offline or
for custom guest images. A clone with no reachable release tag (e.g. a
shallow clone) falls back to the locally built image.
Usage
CLAUDE_CODE_OAUTH_TOKEN=... doublecheck check \
--target DIR # tree under inspection, mounted read-only; default: cwd
--checks-dir DIR # repeatable; default: $TARGET/.agents/checks
--context FILE # run brief: intent, nuances, sanctioned exceptions, scope
--agent NAME # claude (default) or codex
--model MODEL # default per agent: claude haiku, codex gpt-5.6-sol
--parallel N # default: 4 concurrent guests
--output DIR # default: $TARGET/.doublecheck
--check NAME # repeatable; default: every check in the checks dirs
--save-jsonl # persist each inspector's raw stream beside its reportCredentials per agent, gathered on the host before any guest boots:
- claude:
CLAUDE_CODE_OAUTH_TOKENis required in the environment and injected into each guest; where it comes from is the operator's business. - codex: no env var — each guest gets a fresh copy of the host's
~/.codex/auth.json(runcodex loginonce). No staleness guard (removed 2026-07-13): accepted risk that a guest-side refresh of near-expiry ChatGPT tokens rotates the single-use token family out from under the host (codex self-refreshes at ~8 days; a mid-run 401 could do the same regardless). Recovery iscodex loginon the host. Guests never write auth state back.
Mining your Claude history into an observation catalog
doublecheck mine walks every Claude Code transcript on the machine and runs
one sandboxed agent per real conversation (≥ --min-turns genuine human
turns) to extract durable engineering preferences into
~/.doublecheck/catalog, mirroring the transcript tree — one
<project>/<session>/observations.md per conversation, frontmatter recording
the source hash so re-runs only mine new or grown sessions. Mining guests get
egress to the selected agent's own API domains only (claude:
*.anthropic.com; codex: *.chatgpt.com + *.openai.com) — the one
reachable destination is the service the agent already sends its context to.
Note what that means for --agent codex: your Claude Code transcript
content flows to OpenAI. Design: docs/2026-07-05-mine-design.md.
CLAUDE_CODE_OAUTH_TOKEN=... doublecheck mine \
--projects DIR # default: ~/.claude/projects
--catalog DIR # default: ~/.doublecheck/catalog
--agent NAME # claude (default) or codex
--model MODEL # default per agent: claude opus, codex gpt-5.6-sol
# (a bad-model mine pollutes a durable asset)
--parallel N # default: 4
--min-turns N # default: 2
--limit N # mine at most N pending conversations
--dry-run # list what would be mined, boot nothingfixtures/planted/ is a tiny target with two planted silent fallbacks (and
two legitimate defaults that must not be flagged) for exercising the tool
end-to-end:
CLAUDE_CODE_OAUTH_TOKEN=... doublecheck check --target fixtures/planted --model haiku
doublecheck check --target fixtures/planted --agent codex # auth from ~/.codex/auth.jsonWhat the inspector sees
The facts of the agent's world, so you can decide how to author checks:
- The agent is one headless CLI process per check, running inside its
own microVM with all approval/permission gates bypassed — the VM is the
boundary. Check agents have unrestricted internet egress.
--agent claude(default):claude -p(model from--model, default haiku) with the pinned toolkit Task (it can spawn subagents), Bash, Read, Write, Edit, Glob, Grep, WebSearch, WebFetch.--agent codex:codex exec(default model gpt-5.6-sol, reasoning effort pinned to xhigh in the staged guest config) with codex's own toolset: shell, apply_patch, plan/todo, subagent spawning (multi_agent), and server-side web search. The staged config disables codex's ambient inputs — no AGENTS.md pickup, no plugin/marketplace fetch — so the piped prompt is its only input, same as claude.
- Its only input is the prompt: a short environment preamble (verbatim in
src/contract.ts) + the operator's run context, when--contextwas given- the check body + the report contract. No session, no conversation history — the check body is the standard; the run context is everything about this particular run (intent, sanctioned exceptions, scope). The report contract also tells the inspector that every line of its report is read in full, findings must be verified, and "no findings" is a perfectly good report.
- The filesystem: the project is bind-mounted read-only at its real host
path — the live working tree, dirty files and
.gitincluded, sogit log/git diff/git blameandrgwork against the real repo; writes to it fail. The guest image also has node 24, curl, and wget. The cwd is a writable scratch dir private to the check; nothing in it survives exceptreport.md. - The output: the prompt tells the agent its chat reply is discarded and
only
./report.mdis read back. The harness imposes no structure on the report — it contains whatever the check body asks for.
pnpm doublecheck # run the CLI from source (tsx)
pnpm test # vitest — pure logic, plus real-guest integration tests with a FAKE agent (never real claude)
pnpm typecheck # tsc --noEmit
pnpm check # biomeDesign records: docs/2026-07-05-doublecheck-design.md, docs/2026-07-05-run-context-and-decomposed-flags-design.md, docs/2026-07-06-codex-agent-design.md. For how a driving agent should use this tool — above all, what a good --context brief contains — see SKILL.md.
Releasing
Manual, via the release GitHub Actions workflow — semantic-release over
Conventional Commits, ported from pggit. Nothing releases as a side effect of
pushing to main.
gh workflow run release -f dry_run=true # preview next version + notes, publish nothing
gh workflow run release -f dry_run=false # ship: npm publish + GitHub Release + tag + guest image
gh workflow run release -f image_version=X.Y.Z # no release: (re)build + push the guest image
# for an existing version (recovery, or refreshing
# the baked-in agent CLIs)A real release also builds Dockerfile.guest for amd64+arm64 and pushes
ghcr.io/alizain/doublecheck-guest:<version> (+ :latest) — the image
installed CLIs pull at runtime. The ghcr package must be public for those
unauthenticated pulls (one-time visibility flip in the package settings after
the very first push). image_version mutates an existing tag in place;
machines that already pulled it keep their cached copy (the runner pulls
if-missing and never re-checks).
- Only
feat:/fix:/BREAKING CHANGE:commits trigger a release and decide the bump (minor / patch / major). Other commit styles ride along unreleased — usedocs:/chore:/plain messages for work that shouldn't ship a version. versionin package.json stays0.0.0-developmentforever — semantic-release derives the real version from git tags. Never hand-bump it.- The pre-publish gate is biome + tsc + unit tests only (
test:unitexcludes*.integration.test.ts, which boots real microsandbox guests CI doesn't have) + tsdown build. Run the fullpnpm testlocally before releasing. - Needs the
NPM_TOKENrepo secret: a token that can publish@alizain/doublecheckwithout an OTP prompt — classic "Automation" type, or a granular token with read/write package access (and 2FA bypass if the account requires 2FA for writes).
Learned the hard way on the first release (2026-07-05):
- A failed
npm publishleaves a stale tag. semantic-release pushesvX.Y.Zbefore publishing to npm. If the publish step fails, delete the tag (git push origin :refs/tags/vX.Y.Z) before retrying — otherwise the retry sees the tag as the last release, finds no new conventional commits, and releases nothing. - The package is scoped because npm forbids unscoped
doublecheck. The name-similarity rule (DoubleCheckanddouble-checkexist) rejects it with a 403 at publish time only — registry lookups 404 as if the name were free. The installed bin is stilldoublecheck;publishConfig.access: publicis what keeps a scoped package from defaulting to private. - npm provenance/OIDC is off on purpose — the presence of
id-token: writemakes npm 11.x prefer trusted publishing and 404 when none is configured (npm/cli#8976). Details in the comment block of.github/workflows/release.yml.
