Imported from zgeoff/auto-mode (
AGENTS.md). Install upstream withnpx skills add zgeoff/auto-mode. Copyright stays with the author.
Agent Guidelines
auto-mode is a permission classifier: a core library and a Claude Code mod. When Claude Code is about to prompt for a tool call, the mod runs the CLI, hands it the pending call on stdin, and reads allow, deny, or nothing on stdout. Its target is a model that is not Claude driving Claude Code.
docs/architecture/overview.md is the authoritative account of how
the pieces fit: the path of a prompted call, the topology, the state auto-mode keeps, and the rules
that hold across every stage. It links one doc per stage and per kind of evidence. Read the overview
and the docs for the parts you touch in full before changing the request path, the policy, or the
mod. Grep locates code, and it does not teach the invariants.
Required reading
The rules a task needs are not repeated here. Load the skill for the work before starting it.
| Skill | Load before |
|---|---|
code-style |
writing, reviewing, or renaming any TypeScript — the shared function-verb taxonomy |
project-code-style |
the same, beside code-style — auto-mode's own verbs and the comment rules |
testing |
designing, writing, or reviewing tests |
project-testing |
the same, beside testing — the preload, host environment, and mod checks |
docs-writing |
writing or editing any committed prose: docs/, READMEs, this file, skills |
Repo structure
The root package is the published auto-mode, and evals/ is a private Bun workspace package
beside it.
src/ the classifier, a folder per stage or concern; only process/ spawns a child
mods/auto-mode/ the Claude Code mod, shipped as raw TypeScript
contract/ the request and verdict shapes the mod and the CLI share; no dependencies
hooks/ the hooks modules and their *.claude-check.ts checks
policy/ the prompt itself, verbatim model input; the formatter leaves it alone
evals/ private package: imports auto-mode by package name, auto-mode/eval for internals
corpora/ the committed corpora; recorded/ holds the model answers the replays read
runners/ the eval:* scripts, which send real model requests
fixtures/ files Claude Code wrote: mod requests from a live session, and the
claude-json.json and mcp-json.json that the MCP server read parses
e2e/ whole-program suites; the cli-* suites spawn the built dist/cli.js under node
mocks/ the MSW handlers for the decision and Messages APIs
test-utils/ shared test helpers and factories, each with its own tests
scripts/ the repo scripts behind the root package.json
docs/
architecture/ what is built
design/ approved parts of the decision model the code does not implement
guides/ steps a user does
runbooks/ contributor procedures
dist/ generated by bun run build; the published artifact
Evaluation reports live in the private zgeoff/auto-mode-evals repository, not here.
scripts/build-secret-rules.ts generates
src/secrets/betterleaks-rules.json, and release-please
writes CHANGELOG.md, so never edit either by hand.
Boundaries
- The
zgeoff/import-zoneslint rule enforces the module boundaries, and.oxlintrc.jsonholds its zones. Every file undersrc/belongs to a zone, so a newsrc/folder joins one in the same PR that adds it. - Never add an
oxlint-disable-next-line zgeoff/import-zonescomment to make an import pass. Change the import, or change the zones in a PR that reviews the boundary. e2e/import-zones.test.tschecks that each boundary still fails a forbidden import.- A hooks module imports only its own plugin's files, by relative path, and
claude-code. Claude Code refuses a package import, its own package included.
Commands
bun run lintis plain oxlint.bun run lint:fulladds the type-aware pass and the type check, and is CI-only: never run it locally.bun run checkincludes it, so leave that to CI too.- lefthook runs the light checks before a commit and a push. Leave the heavier checks to CI, because the host may have little memory.
- Invoke a gate by its root
package.jsonscript name, never by re-spelling the command. - Pin exact versions, with no
^or~ranges.bun addsaves exact versions, so the rule covers hand-written edits.
Runtime rules
- Use Bun for development and node for the artifact, and
bun test, never vitest. The publisheddist/runs under node 24 on a machine that may have no bun. runandrecord, the two commands the mod runs, always exit 0. The mod reads a non-zero exit as a failure, and the JSON on stdout decides the outcome.- A usage error, such as an unknown command or option, exits 2 with the usage on stderr. No mod call reaches it, and a caller by hand needs to see the mistake.
- Writing nothing is a verdict, not a failure: auto-mode has no opinion, and the mod keeps the
prompt Claude Code was about to show. These cases write nothing:
- a body that is not a mod request
- a body that is not JSON
- a failed model call under
onFailure: "defer" - the action that exceeds the denial budget
- A classifier decision is always an allow or a deny with a reason, and it never asks. The denial budget is the only path to a human.
- Runtime dependencies are bundled, not external. The CLI starts once per prompted tool call, so
resolving a package from
node_modulesis paid on every call.
The policy
Everything under policy/ is verbatim model input. Its bytes are the prompt and the cache prefix,
so a reflow is a prompt change and belongs in a commit that reviews it as one.
decision.mdis the Jev framework.judge.mdis the judge's framework.rules.mdis the rule list.loadPolicysplices it in at the<rules>marker.containment.mdis the Outside Task Scope rule the judge reads when it reviews a containment deny the user named.denial.mdis the instruction every deny reason ends with. It reaches the agent, not a classifier.auto-mode print-promptprints the selected base policy. Configured rules and action evidence are separate request fields.
Rules for the policy text:
- Every instruction to block terminates at a rule name that exists in
rules.mdor at an explicit configured deny entry. An evaluation rule in a framework never orders a block on its own, because a block the model cannot name is one the user cannot read or appeal. A block that cannot be named is an allow. - No evaluation rule shares a name with a rule or exception, or prefixes one, because a verdict quotes the name back and the reader cannot tell which was meant.
- The local tier never denies on a rule, because the rules that deny are prose that needs a reader. It denies only on a concrete scope finding: a write target outside the task scope, named in the deny. Everything else it allows or escalates.
Mod contract
- Claude Code is the only harness auto-mode targets. Codex and Muse ship their own auto mode, so a feature may depend on what only Claude Code offers.
- The mod judges only an
askfrom the rest of Claude Code's permission chain. An existing allow or deny is final. - The mod's request carries the session identity, the action, and the task context, including the last direct user message. auto-mode reads no transcript.
- The CLI and the mod ship together, so the request and the verdict are strict. A shape either side does not know keeps the prompt.
mods/auto-mode/contract/types.tsholds both shapes, and a type test fails when it and the CLI's schema disagree.mods/auto-mode/hooks/parse-decision.tsalone maps the CLI's verdict to Claude Code's permission decision.- auto-mode never writes Claude Code's settings files.
- Verify a change to the mod contract by hand in a live Claude Code session before it merges. The fixtures are recordings, not a substitute for running it.
Banned words
The repo bans these words in all prose. check-prose.sh fails on each one except bites and
floor, which need judgment.
bitesCASceilingfence,fencingfloorload-bearingseamsurface
