defend-cli
v0.8.0
Published
Never ship code you can't defend. Automated whiteboard defense for AI-assisted code: a fresh Claude Code or Codex session interviews the author about the change and verifies the answers against the repository.
Maintainers
Readme
Defend
Never ship code you can't defend.
AI can write the code. Defend checks whether you can defend it.
Before a change ships, Defend starts a fresh reviewer session (Claude Code or Codex), reads the diff and the surrounding repository, and interviews the author the way a good engineering manager would at a whiteboard:
Why did you go with a new
NotificationQueuehere? We already haveOutboxPublisherinservices/outboxthat does retries. What does the new one do that it doesn't?
You type the answer. Defend verifies your claims against the code, asks a follow-up when something doesn't add up, and writes an attestation bound to the exact diff. No per-developer scores, no leaderboards: per-change assurance and a private knowledge map.
Inspired by Mitchell Hashimoto's whiteboard defense.
Install
npm install -g defend-cliRequirements on the machine that runs the defense:
- Node.js 20+
- a logged-in Claude Code CLI (
claude) or Codex CLI (codex). Defend uses whichever you pick; it has no backend and no API key of its own. Inference is billed to your existing subscription.
Your source code never leaves the machine. The reviewer runs locally with read-only tools.
Quick start
cd your-repo
defend init # personal mode: config + skills, excluded from git, nothing to commit
git add -A
defend # defends the staged diff
defend --base main # defends the whole branch since it diverged from maindefend init is optional; defend works with no files in the repository at all. Personal mode keeps everything
it writes (defend.yml, the skills, .defend/) out of git status through .git/info/exclude. When a team wants
shared config and CI enforcement, defend init --team makes the same files committable and --github adds the
workflow.
A defense looks like this:
🛡 Defend never ship code you can't defend
Files changed: 14
Lines: +842 / -126
Risk: HIGH
Primary concern: state lifecycle
Blast radius: HIGH
You introduced a `processing` state between `pending` and `complete` for vendor jobs, and the
worker now claims jobs by moving them into it.
Before shipping, demonstrate that you understand it.
4 questions · ~4 min
────────────────────────────────────────────────────────────
1/4 State · blocking
Which parts of the system assume a job moves straight from pending to complete, and why does the
new state not break them?
> the worker only selects pending jobs, so processing stops double pickup.
the frontend only cares whether it is complete.
⚠ Follow-up
The worker matches your answer (worker/claimJob.ts:41). But BillingService treats every
non-complete job as a billable pending job (billing/BillingService.ts:118). What happens while a
job is processing?Answers are typed. Pasting is rejected by default (input.paste: deny), because the point is to reconstruct your
own mental model, not to relay one from another chat window.
Commands
| Command | What it does |
|---|---|
| defend | Run the defense for the staged diff (default command). |
| defend --base <ref> | Run it for everything on the branch since <ref>. |
| defend doctor | Checks git, the reviewer CLI, its login (with a sub-cent round trip), the signing key and the config. Run it first. |
| defend check | Files, diff hash, deterministic risk signals and matched rules. No AI. |
| defend map [path] | Who can defend this code: per-file coverage from passing defenses, with knowledge decay. --all across repositories. |
| defend status | Is there a passing attestation for exactly this diff? Exit code 0/1. |
| defend stats | Usage and knowledge map from .defend/attestations/. --me scopes to you, --json for tools, --html writes a dashboard page, --repos <a> <b> aggregates several repositories into a company view. |
| defend verify | CI gate. Exit 0 only when a passing, correctly signed attestation exists for exactly this diff. |
| defend report | The PR table as Markdown, for a GitHub comment. |
| defend init | Personal mode by default (nothing to commit). --team for shared config and committed attestations, --ci github\|gitlab\|azure adds the CI template. |
Useful flags for defend: --agent claude-code|codex, --model <id>, --effort low|medium|high|max, --budget <usd>,
--max-questions <n> (flat cap over the per-level budget), --paste deny|warn|allow, --yes (skip the large-change
confirmation), --force.
Cost
Defend has no backend; the reviewer runs on your Claude Code or Codex subscription and the cost is yours. Measured
with the defaults (Claude Code, sonnet analysis, haiku grading) on real repositories, October 2026:
| Change | Analysis | Per graded answer | Per escalated check | Typical total | |---|---|---|---|---| | 2 files, +2/-15 (service + test) | $0.26–0.34 | $0.05–0.06 | $0.15–0.19 | $0.45–0.60 for 2–3 questions | | 6 files, +223/-17 (import-cycle fix) | $0.06–0.25 | $0.05 | ≈ $0.20 | ≈ $0.40 | | 17 files, +1103 (seeded data + code) | $0.26 | — | — | analysis only | | 67 files, +13.8k (large branch) | ≈ $0.30 | $0.12–0.16 | — | ≈ $0.60 for 1 question |
Codex reports no cost, so the same runs show turns and cached tokens but no dollar figures. Defaults are chosen to keep a typical PR well under a dollar:
- Model
sonnet, effortmediumfor Claude Code. Codex uses the model in your~/.codex/config.toml. budgetUsd: 1is a hard stop for the whole defense. Before every reviewer turn Defend checks that the previous turn's cost still fits, and the remaining amount is passed to the reviewer as its own ceiling, so a run cannot overshoot by a full turn. (Codex reports no cost, so the budget cannot be enforced there.)- Nothing is lost at a stop. A budget stop or
/quitwrites the attestation asincompletewith the questions answered so far and keeps the reviewer session. Runningdefendagain on the same diff resumes from the next question without paying for the analysis twice;--restartbegins over. Accepting a risk costs nothing: it is recorded locally without a reviewer turn. - Per-risk overrides let you pay for a stronger reviewer only where it matters, e.g.
overrides: { critical: { model: opus, effort: high } }. The analysis turn always runs on the cheap default; the override applies to the answer turns once the risk is known. timeoutSeconds: 180per reviewer turn (the analysis turn gets twice that). A reviewer that cannot answer fails loudly instead of spinning.- Before the run the header shows model, effort, budget and timeout; after the analysis it shows what is left per question. After every answer you see the cost of that turn against the budget; the summary shows cost per question and one line with total spend, reviewer turns and tokens read from cache. Changes over 25 files or 1,500 lines ask for confirmation first.
- Answers are graded, not re-investigated. The analysis turn, which already read the code, writes a hidden
reference answer per question (what a correct answer must establish, expected concepts, fatal misconceptions,
file:line). Each of your answers is then graded by a tool-free call on a small model (
haikuby default) against that reference: about a cent instead of a full agentic turn. Only when you make a specific claim the reference does not cover (another caller, a guard, a lock, a test) does Defend escalate to the reviewer and check that claim in the repository.verification: auto | cheap | fullindefend.ymlor--verificationselects this;fullrestores the old behavior of verifying every answer in the repository. - After the analysis the header says how many questions the remaining budget covers, so a short budget is visible before the first answer rather than mid-defense.
If the reviewer cannot run (expired login, missing CLI), defend exits with the reason and the command that fixes it.
defend doctor checks the same things up front.
During a defense: an empty line submits the answer, /accept <reason> records an accepted risk instead of answering,
/useless <why> flags a pointless question (recorded, never counted against you), /quit pauses without losing
anything. After a graded FAIL you can type /dispute <why>: the strong reviewer re-checks that one verdict in the
repository (once per question) and overturns it if you were right. After a failed defense, defend --only-failed
re-asks just the failed questions on the same diff, without a new analysis.
Every question is blocking or advisory. Blocking means a wrong answer leaves an unguarded production failure
mode (double side effects, data loss, auth bypass, money); at most one or two per defense. Advisory marks an
understanding gap worth closing; it is recorded but never fails the defense. A short answer that establishes the
expected points passes; follow-ups deepen, they do not gate.
Every file:line the reviewer shows you is checked against the working tree first: a file that does not exist or a
line past the end of the file is marked as such instead of printed as fact, and the grader sees the actual text of
each cited line next to the reference. This is deterministic and cheap; it cannot tell whether a valid line says what
the reviewer claims, which is what /dispute is for.
How it connects to Claude Code and Codex
Defend is an orchestrator over the coding agent you already have. It does not index your repository or call a model API directly.
| | Claude Code | Codex |
|---|---|---|
| Invocation | claude -p with a JSON schema for structured output | codex exec --json --output-schema |
| Repository access | read-only tools only (Read, Grep, Glob, git diff/log/show/blame) | --sandbox read-only |
| Coherent session | --resume <session> for every answer | codex exec resume <thread> |
| Isolation | no MCP servers, no project hooks, user settings only | project MCP config still loads (harmless) |
| Fresh context | always a new session, never the one that wrote the code | same |
The reviewer that interviews you is never the session that generated the code: the same repository, a fresh conversation, so it has to infer the design from the code like a colleague would.
Both adapters implement one interface (DefenseAgent.runTurn) in src/agents/. Adding another provider is one file.
Using Defend from inside Claude Code or Codex
defend init installs a defend skill in .claude/skills/ and .agents/skills/. Inside either tool you can then
say "defend this change" or "show my defend dashboard". The skill knows that the defense itself is interactive and
must run in your terminal, and that the dashboard is defend stats --me --json, which it summarizes as a short
knowledge map: defenses, pass rate, minutes, and the categories where your follow-ups cluster.
Configuration
defend.yml in the repository root. Everything is optional.
version: 1
reviewer:
provider: claude-code # claude-code | codex
model: sonnet # cheap default
effort: medium # low | medium | high | max
overrides:
critical: { model: opus, effort: high }
verification: auto # auto | cheap | full
grader:
model: haiku # tool-free grader for answers
effort: low
budgetUsd: 1 # hard stop for the whole defense
timeoutSeconds: 180 # per reviewer turn
questions: # budget per risk level; the reviewer asks fewer when there is less to ask
low: 1
medium: 3
high: 5
critical: 6
maxFollowUps: 2
input:
paste: deny # deny | warn | allow
ignore:
- "**/*.lock"
- "**/generated/**"
rules:
- name: payments
paths: ["**/payment*/**", "**/billing/**"]
risk: critical
require: [data_integrity, security, failure_mode]Rules are optional and only ever add scrutiny. Every change in every folder is analyzed by the reviewer, with or
without rules. A matching rule does two things: raises the risk floor for the touched paths, and requires at least
one question from each listed category. Rules never restrict what is analyzed and never supply the question text.
The only thing that excludes files is the ignore list. Questions are always generated from the actual change and the actual
repository, so a rule for payments produces a question about your payment change, not a checklist item.
Question categories: architecture, failure_mode, blast_radius, security, data_integrity, concurrency,
state, testing.
Attestations
A passing defense writes .defend/attestations/<diffHash>.json:
{
"author": { "name": "Jasmin", "email": "[email protected]" },
"reviewer": { "provider": "codex" },
"diffHash": "e3ef985bf51d…",
"risk": "high",
"result": "pass",
"categories": { "state": "pass", "failure_mode": "pass", "security": "accepted_risk" },
"questions": [{ "id": "q1", "category": "state", "status": "pass", "followUps": 1, "typedSeconds": 74, "pasteAttempts": 0 }]
}Every attestation is also mirrored to ~/.defend/history/<repository>/, so defend stats --all shows your
knowledge map across every repository on the machine without committing anything. In team mode the repository
folder is committed too: the attestation travels with the branch and CI can verify it. Answers themselves are
never stored.
Every attestation is signed (HMAC-SHA256 over its canonical JSON). The key comes from DEFEND_SIGNING_KEY, or a
per-machine key generated on first use. Editing an attestation after the fact makes defend verify reject it.
What the attestation is: evidence that a named person defended this exact diff, with typing evidence per answer. What it is not: proof that nobody helped. It is friction and a record, not an anti-cheat mechanism.
CI enforcement
defend verify and defend report only need git and a shell, so they run on any CI. defend init --ci <provider>
writes a ready template:
| Provider | --ci | File | Comment posted with |
|---|---|---|---|
| GitHub Actions | github (or --github) | .github/workflows/defend.yml | gh pr comment (built-in token) |
| GitLab CI | gitlab | .gitlab-ci.defend.yml, include it from .gitlab-ci.yml | MR note via API, optional DEFEND_GITLAB_TOKEN |
| Azure Pipelines | azure | azure-pipelines.defend.yml, add as build validation | PR thread via REST with System.AccessToken |
Anything else (Bitbucket, Jenkins, Buildkite): run defend verify --base origin/<target> as the gate and pipe
defend report --base origin/<target> into whatever posts comments on your platform.
On every pull request the template runs defend verify --base origin/<base> and posts defend report:
🛡 Author Defense
Risk: HIGH · Result: ✅ PASS · Defended by: Ana
| Dimension | Result |
| State | ✅ |
| Failure mode | ✅ |
| Security | ⚠️ accepted risk |
Questions 3 · Follow-ups 1 · 184s · reviewer claude-code
Defense verified for diff e3ef985bf51d · Signature validMake the check required (branch protection, merge request approval rule, or build validation policy) and a change
cannot merge until its author has defended the exact diff under review. The team shares one signing key: export
DEFEND_SIGNING_KEY on developer machines and add it as the CI secret of the same name.
Who can defend this file
Every passing attestation records the files it covered. defend map turns that into a coverage map of the codebase:
for each file, who defended it, when, and which categories (state, failure modes, security...) they held up under.
Knowledge decays: a defense counts fully today and halves every 90 days, so the map stays honest after churn.
$ defend map src/payments
● src/payments/refund.ts
Ana ×3, Marko · 1d ago · Failure mode, Security, State
● src/payments/charge.ts
Dev ×2, Ana · 3d ago · Architecture, Data integrity, Concurrencydefend itself warns when a change touches files nobody has ever defended: if AI wrote it, no human on record
understands it yet. Use the map to pick reviewers, to answer "where are we exposed?", and to see which modules a team
would struggle to explain. It is a map of the code, not a ranking of people.
Dashboards
defend statsin the terminal,defend stats --mefor yourself,defend stats --allacross every repository you defended on this machine.defend stats --htmlwrites a self-contained page: defenses per week, confidence by category, usage and cost per reviewer, adoption per author (activity only), accepted risks, recent defenses.defend stats --html --repos ~/src/api ~/src/web ~/src/mobileis the company view across repositories, which is what you want when the organisation pays for the Claude or Codex subscription and wants to see how the tool is used. No per-person understanding scores, by design.- Inside Claude Code or Codex: "show my defend dashboard" (installed by
defend init).
Why not just write a skill that asks questions?
You can, and it will ask decent questions. What a skill cannot do:
- Be independent. A skill runs inside the session that wrote the code, with that session's context and assumptions. Defend starts a fresh reviewer that has to infer the design from the code, like a colleague would.
- Produce a record anyone can trust. Defend binds a signed attestation to the exact diff hash and the git author,
and
defend verifyrejects anything edited afterwards. A chat transcript proves nothing to CI. - Block a merge. The GitHub workflow turns the attestation into a required check. Policy (
rules, required categories, risk acceptance) lives in the repository, not in someone's prompt. - Work the same for everyone. The same defense whether the author uses Claude Code or Codex, with typed answers and paste policy, so the team gets one standard instead of twelve personal prompts.
- Learn.
/uselessfeedback, follow-up rates and accepted risks accumulate into a question-quality metric and a knowledge map across repositories and time. That data is the product; a skill starts from zero every time.
Privacy
Source code, diffs and answers stay on the machine that runs the defense. The only durable output is the attestation above. There is no telemetry.
Status
Early. The core loop (fresh reviewer, evidence-backed questions, typed answers, verification against the repository, diff-bound attestation) works with both providers. Not yet built: a GitHub check that renders the attestation on the PR, signed attestations, and organization policies.
License
MIT
