anti-hall — extended guide¶
Detail moved out of the READMEs during the v0.107.0 doc sweep so the top-level READMEs stay a scannable landing page. Nothing here is deleted — every fact that used to live in README.md / plugins/anti-hall/README.md is preserved below, verbatim where practical.
New in 0.108.0 (full list: CHANGELOG): automatic handover at
85% context (auto-handover.js, auto-handover-pause-nag.js); one settings store and the
/anti-hall:settings skill (Settings); repairs on every
reload (repair-on-reload.js); update-vs-reload version alerts; Jev cost, precision,
budget watch and credit balance; and for DevSwarm the app database as ground truth
(KB-devswarm-app-db.md), a capability gate, auto-archive,
owner-approved prune and message retention. Per-component detail is in the
Features table.
Contents¶
- What it is, and why it exists
- What it blocks, and how to turn it off
- Limits and escape hatches
- Network and data
- Install, verify and uninstall
- Requirements
- Capabilities at a glance
- What's inside and updating
- Hook reference (detailed) — how it works, per mechanism
- Skills reference (detailed)
- DevSwarm (current state)
- Contributing / testing (plugin)
- Features table — every hook and component
- Statusline
- Settings (
/anti-hall:settings) - Configuration / tuning
- Troubleshooting / FAQ
- Test locally
What it is, and why it exists¶
anti-hall is a Claude Code marketplace + plugin, plus a separate Codex-native port,
that keeps coding assistants from acting before they verify. It ships always-on Node
hooks (mechanical guards no prompt can talk around), a set of evidence-driven workflow
skills, and a live two-line statusline. A small Rust engine answers the hooks with Node.js 22+ as the fallback, no npm dependencies, macOS · Linux
(Node ≥ 22 on PATH is the only prerequisite; Windows is not supported).
It targets four predictable failure modes: eagerness (acting before investigating), hallucination (stating unverified facts as truth), fix-before-diagnosis (patching a symptom before proving the cause), and fake completion ("done" without running the check). See why it exists, and what's proven vs. not for the eval behind that claim.
Before and after. An assistant, mid-task, runs a force-push to main. Without anti-hall, the published history is rewritten. With it, the command never runs and the assistant is told:
anti-hall git-guard: BLOCKED. Force push detected. Rewriting published history is a deliberate human action - do it manually with explicit owner confirmation, never from an automated push.
The Claude plugin is the authoritative package; the Codex port
(plugins/anti-hall/codex/) is a separate, intentionally non-1:1 mirror (different
hooks.json, different skill set (anti-hall-*), same underlying guards where payload
contracts are verified for Codex) — see
plugins/anti-hall/codex/README.md for hook parity
and Codex / cross-tool for the Codex/OMX detail.
What it blocks, and how to turn it off¶
The main guards. Every one can be switched off with its setting (/anti-hall:settings or node plugins/anti-hall/scripts/settings.js set <key> false); the /config panel only has the four safety.* switches among them. The four safety.* keys are locked: changing them needs --confirmed. Separately, you can tell the assistant to skip a guard for a short time; it records that in ~/.anti-hall/skip.json (per guard name, expires after 15 minutes by default; "all" never covers git-guard, which must be named).
| Guard | What it stops | Why | Turn off |
|---|---|---|---|
git-guard |
Force-push and AI self-credit in commits and gh PR/issue/release text |
Rewriting published history and AI co-author trailers are human decisions | safety.gitGuard; skip name git-guard |
command-guard |
The main session running heavy commands (build, test, deploy, push) itself | Keeps the main conversation free; helpers do the long work | safety.commandGuard; skip name command-guard |
edit-guard |
The main session editing files outside its own plan/state/handover files | Edits go through a helper that can be checked | safety.editGuard; skip name edit-guard |
swarm-guard |
Agent spawns past the spawn-rate cap or under critical memory pressure | Prevents a fork-bomb of agents overloading the machine | safety.swarmGuard; skip name swarm-guard |
api-guard |
Written code that calls a standard-library or built-in API that does not exist | Catches invented functions before they land | guards.apiGuard; skip name api-guard |
speculation-guard |
A turn that ends on unverified, hedged claims | Claims need evidence, not "probably" | guards.speculationGuard; skip name speculation-guard |
output-verify-guard |
Unverified completion claims | "Done" needs a check that was actually run | guards.outputVerifyGuard |
task-guard / tasklist-guard |
Stopping while tracked tasks are open; multi-step work with no task list or progress file | Stops half-finished work being reported as finished | guards.taskGuard / guards.tasklistGuard; skip names task-guard / tasklist-guard |
Everything else, with the exact behaviour of each hook: the Features table.
Limits and escape hatches¶
Guards are pattern- and rule-based. They can block or nag when they should not, and they can miss things. This section lists the switches, the known gaps and the costs.
Escape hatches¶
| To do this | Use |
|---|---|
| Skip one guard for a short time | Tell the assistant to skip it. It writes ~/.anti-hall/skip.json (per guard name, 15 minutes by default). "all" covers the noisy guards but never git-guard; name git-guard to skip it. See User-override escape hatch. |
| Turn a safety guard off | safety.gitGuard, safety.commandGuard, safety.editGuard, safety.swarmGuard. Changing them needs --confirmed. |
| Turn another guard off or down | guards.*, for example guards.apiGuard, guards.speculationGuard, guards.modelRouting = advisory or off. |
| Silence an advisory message | guards.failureRootCauseNudge, guards.silentAgentNudge, guards.scanThrottle, codexNudge.enabled, autoHandover.nag, limitConserve.mode = off. |
| Acknowledge a confirmed false positive | guards.stopAck lets a session ack it for silent-agent-nudge and tasklist-guard. |
| Get the full protocol text | context.protocolLevel = full (env ANTIHALL_PROTOCOL_LEVEL=full). |
The full key list is in Settings.
Protocol text size¶
The injected protocol text is compact by default. The session-start core keeps every load-bearing clause and points at the generated plugins/anti-hall/PROTOCOL.md for the full wording. context.protocolLevel = full restores the previous text byte for byte. Sizes are in the CHANGELOG (a synthetic size measure, not a per-run saving).
Remaining limits¶
False positives that were fixed are listed in the CHANGELOG. What is still true:
- Shell commands are parsed, not executed.
git-guardresolves git and shell aliases, but it reads the command text; it does not run it. - Heredocs and indirect runs in
git-guard. A heredoc written to a prose/data file (.md,.txt, ...) or used as a commit, tag or PR body is treated as data and not scanned as commands, provided every git/gh beside it uses only a short flag allowlist. Commands run throughxargs,parallel,find -exec,flockand wrappers such asstdbuforsetsidget the same checks as a direct run. Still not covered: a subcommand that comes only from stdin (cat f | xargs git) cannot be seen, andgit reset --hardhas no rule. A script written by a heredoc is still scanned, even when it is never run. - Commit messages. A message taken from an existing commit (
-C,--amend --no-edit, a template) is checked before the commit. A message written in an editor, or changed by acommit-msghook, does not exist yet at that point; it is audited after the commit, so the commit is not blocked. - Shell writes. Writes made through the shell (
cat >,tee,sed -iand similar) reachedit-guard,api-guardandship-it-guard. Some forms still fail open: a variable or glob as the target,dd,installandrsync, and scripts that write when they run. speculation-guardis lexical. It catches hedge words. It does not catch a confident claim with no hedge word. Two opt-in checks target that gap; see the next subsection. Both are off by default.- Prompt text alone does not reduce fabrication. The verify-first eval found no net fabrication reduction from the prompt alone in four runs (tools/eval/README.md). The guards' blocking is covered by unit tests, not by that eval.
- Advisory noise. Replaying 5,828 real root-cause nudges from 30 days of transcripts, 3,438 still fire after the filter.
output-verify-guardfires at most once per turn for the same signals. Both can still be wrong; use the switches above. - Codex.
edit-guard,api-guardandship-it-guardcoverapply_patchedits and shell writes on Codex 0.134 or later. The fail-open shell forms above apply there too. See the Codex port notes.
Opt-in checks for confident claims¶
| Check | Setting | Precision, synthetic corpus | Precision, real replies | Cost | Default |
|---|---|---|---|---|---|
| Inference check | guards.inferenceCheck |
1.00 (recall 0.95) | 0.45 or lower (estimate from a 40-flag sample) | none (no model call) | off |
| Judge, keyless backend | jev.semanticJudge on, jev.judgeBackend = cli |
0.78 to 0.81 (recall 1.0) | not measured | 5 to 6 s per turn end, measured | off |
| Judge, API backend | jev.semanticJudge on, jev.judgeBackend = api |
not measured | not measured | about 1 to 3 s and $0.0001 to $0.001 per turn end, estimated | off |
The synthetic corpus is 84 labelled cases (tools/eval/inference-bench.js), written by the same author as the detector, so read it as a regression floor. Both checks stay off because of these numbers: the inference check was wrong more often than right on real replies, and the judge adds a visible delay to every turn end. Details: speculation-judge.
Hook latency¶
The numbers below are for the Node hooks. With the engine running most calls skip the Node start-up; its pre-release replay numbers are in AH-ENGINE.md and are not part of these tables.
Hooks are small per call but not free. On a quiet machine a bare node -e 0 costs about 16 to 18 ms of CPU and most hooks cost 22 to 35 ms. Claude Code runs a matcher's hooks in parallel, so a tool call costs about its slowest hook, while CPU adds up across hooks. The DevSwarm hooks exit before loading their libraries in a session that is not a DevSwarm Primary or child, which saves about 20 ms of CPU per Stop and about 25 ms per prompt. The measured tables, method and caveats are in HOOK-LATENCY.
The engine¶
ah-engine, a small Rust program, is the core component: it answers the hook calls without starting Node per call. The plugin's hooks are one
thin trigger per event; the engine decides natively what it can prove identical to the Node hook and defers the rest to Node (never
weaker than Node). The Node hooks are a temporary fallback for a missing or failed binary and are removed in v1.0. The binary is downloaded once
from the GitHub Release by a shell bootstrap and installed only if its sha256 equals the one pinned in the plugin's ah-engine.lock
(the setting engine.bootstrap = false, or AH_ENGINE_BOOTSTRAP=0, skips it). All its rules, settings and texts are plain files in plugins/anti-hall/engine/, read at run
time with hot reload and fallbacks (edited, then last-known-good, then pristine, then Node), and ah-engine config heal restores a
missing key. Still on Node: the DevSwarm mesh writes and daemons, every call that consults Jev, the semantic judge's model call and the
statusline. macOS and Linux only; Windows is not supported yet.
Full description (install, go-live, rollback, failover, telemetry, what runs on Node, pre-release measurements): AH-ENGINE.md.
Network and data¶
The short form is in the README; the full table is PRIVACY.md. Nothing
here goes beyond it: no analytics and nothing reported to anyone; one default-on update check (a tag-list request to
github.com/talas9/anti-hall, no project data; off via versionAlerts.antiHall or
ANTIHALL_VERSION_ALERT=off); a one-time download of the ah-engine binary from the GitHub Release
(sha256-pinned in the plugin, nothing about you sent; off via the setting engine.bootstrap or AH_ENGINE_BOOTSTRAP=0); local-only engine usage counters
(identifiers and counts, never content; telemetry.enabled; read with ah-engine telemetry summary); the Jev classifier, the semantic judge
(jev.semanticJudge or ANTIHALL_SEMANTIC_JUDGE=1) and mesh message triage are off by default and send the
text they judge only to the provider you configure. API keys come from sensitive plugin
options; reading a key from the environment or a key file is opt-in
(jev.allowLegacyKeyRead, guards.allowAnthropicEnvKey), and the Jev endpoint override
is loopback-only. Everything else (logs, handovers, defect reports) stays in
~/.anti-hall/ and <repo>/.anti-hall/. gh, codex and hivecontrol run under your
own accounts; the plugin does not spawn gh or codex.
Install, verify and uninstall¶
Prerequisite: Node.js >= 22 on PATH (check with node --version).
Claude Code:
To try it without installing: claude --plugin-dir /path/to/anti-hall. The hooks apply
globally once enabled; the statusline is a separate one-command install
(Statusline).
Codex (from scratch; the installer lives in the repo, so clone it first):
git clone https://github.com/talas9/anti-hall.git
cd anti-hall
node plugins/anti-hall/codex/install-codex.js # project-local: writes ./.codex/hooks.json
node plugins/anti-hall/codex/install-codex.js --global # or user-wide: writes ~/.codex/hooks.json
Add --dry-run to preview. The installer merges into an existing hooks.json, backs up any file it changes (.bak-<timestamp>), and enables [features] hooks = true in the matching config.toml.
Git-ignore the state directory: anti-hall writes per-project session notes (progress, history, handovers, reports) under .anti-hall/ in your repo, and never edits your tracked files. Add .anti-hall/ to your project's .gitignore so a git add . can't commit them (or run /anti-hall:doctor --repair, which appends it to the untracked .git/info/exclude).
Verify it worked: in Claude Code, ask "is anti-hall working" (runs the doctor skill: live self-tests on every guard), or run /anti-hall:settings to see the active settings. From a clone you can also run node plugins/anti-hall/hooks/doctor.js --check.
Uninstall¶
Claude Code:
claude plugin uninstall anti-hall@anti-hall
claude plugin marketplace remove anti-hall # optional: also drop the marketplace
If you installed the statusline, remove it first with the install-statusline skill's uninstall, or run node statusline/uninstall-statusline.js from the plugin directory (it restores your previous statusLine).
Codex: the installer has no uninstall flag. Open .codex/hooks.json (project) or ~/.codex/hooks.json (global) and delete the hook groups whose command path contains /plugins/anti-hall/hooks/; the installer's .bak-<timestamp> copies hold your prior file. The [features] hooks = true line it added to config.toml is left alone.
Optional companions (only if you installed them; run from plugins/anti-hall/companion/ in a clone or the plugin directory):
node install-reaper.js --uninstall # MCP orphan reaper (macOS LaunchAgent com.anti-hall.mcp-reaper / Linux systemd --user timer)
node install-devswarm-supervisor.js --uninstall # DevSwarm liveness supervisor (com.anti-hall.devswarm-supervisor)
node install-devswarm-ingest.js --uninstall # DevSwarm ingest daemon (com.anti-hall.devswarm-ingest)
~/.anti-hall/ holds your settings, skip file and logs; it is not removed automatically. Delete it yourself only if you want that state gone.
Requirements¶
Node.js ≥ 22 on PATH. Every hook and the statusline are pure Node (built-ins
only), launched as node <hook>.js (directly, or by ah-engine for the cases it hands back to Node). No node on the hook shell's PATH means Claude
Code silently skips every anti-hall hook — verify with node --version. No npm
install, no native deps, no other config. There is intentionally no shell-based
preflight. Install Node from https://nodejs.org.
Supported systems: macOS and Linux (both CI-tested), including WSL on Windows, which runs the Linux build. Native Windows is not supported yet.
The /config rows (14 options) need Claude Code ≥ 2.1.269; older versions still work via the skill.
The plugin's userConfig never declares options (a public plugin can't require v2.1.271+
just for its settings UI — per the Claude Code plugin docs, an options picker would break
loading on older versions), so every version can load the plugin; enum settings just render
as a plain string field describing the allowed values instead of a picker.
Capabilities at a glance¶
Terms used below: DevSwarm is a multi-workspace orchestration app with a hivecontrol CLI; anti-hall's integration is optional and dormant unless you use it. Jev is TypeSafe's "System One" decision model, used as an opt-in classifier. deadly-loop is a parallel Reviewer + Auditor + Critic debate with fix waves, run before merging risky changes.
| Area | What it does |
|---|---|
| Guards | Mechanical, always-on Node hooks — block AI self-credit/force-push (git-guard), fabricated stdlib/builtin APIs (api-guard), un-delegated heavy commands (command-guard), un-delegated direct edits (edit-guard), spawn-rate/memory fork-bombs (swarm-guard), stopping with open tasks (task-guard/tasklist-guard), cheapest-fitting-model routing (model-routing-guard), opt-in merge-gate/ship-it-guard, and more. |
| Verify-first discipline | Injects the full Iron-Law + rationalization-table protocol at session start (survives compaction) and a rotating one-line nudge every turn, plus the always-on scope-fidelity + anti-sycophancy rules; enforced, not just suggested. |
| Orchestration | Non-blocking coordinator discipline: delegate heavy/broad work to subagents, verify delegated "done" claims against ground truth, live phase progress on the statusline. |
| Auto-handover | On by default: at 85% context the agent writes a handover itself, tells you, and suggests /compact or /clear; short follow-up reminders after that. Configure via /anti-hall:settings. |
| DevSwarm mesh | Optional, dormant unless a DevSwarm session is active — layered wake/recovery (self-report → poke → escalate, never auto-kill), one mailbox per session, per-turn mesh status. The DevSwarm app's own database is the ground truth for workspace state; done workspaces are auto-archived (DevSwarm ≥ 2.5.3), old message bodies are archived then pruned. See KB-devswarm-hivecontrol.md and KB-devswarm-app-db.md. |
| Jev classifier | Optional LLM-backed speculation classifier (KB-jev-classifier.md) — per-integration on/shadow/off modes, metrics + jev report KEEP/REVIEW/REMOVE calls. |
| Statusline | Live two-line statusline: git/model/context/cost on line 1, live orchestration/context gauge on line 2. Installable globally or per-repo, consolidates with an existing statusline (e.g. OMC HUD). |
| doctor / update | doctor runs live behavioral self-tests on every guard and repairs safe drift; update pulls the latest release and shows the changelog delta. Repairs also run by themselves after a plugin reload or on a new version. |
| Settings | One place for every setting — ~/.anti-hall/settings.json, grouped by category and reachable through /anti-hall:settings (Claude Code's /config panel has only the headline switches, the safety guards and the keys); or tell /anti-hall:settings "set X to Y". See Settings. |
What's inside and updating¶
Repo layout, the AGENTS.md cross-tool mirror, the node --test suite, and the
/anti-hall:update flow are documented in this guide (Codex / cross-tool,
Test locally, the update skill under Skills reference).
The full plugin component reference is plugins/anti-hall/README.md.
Release notes: CHANGELOG.md.
Hook reference (detailed)¶
Moved from plugins/anti-hall/README.md "How it works" (v0.107.0 doc sweep).
How it works¶
Verify-first protocol (the core)¶
- SessionStart protocol — by default (
context.protocolLevel=compact)verify-first-full.jsinjects a compact core that keeps every load-bearing clause inline and points at the generatedPROTOCOL.md;context.protocolLevel=fullinjects the FULL verify-first + root-cause protocol described next, byte for byte, in the Superpowers Iron Law + rationalization-table form. It names the specific bypass excuses ("probably", "should work", "seems to", "I'll just assume", "looks done", "tests pass on first run") and includes a skill primer listing the core 4 skills (root-cause, orchestration, deadly-loop, ship-it) and when to reach for each. It also carries the always-on output-presentation rule K ("PRESENT FOR SCANNABILITY"): organize output with GitHub-flavored markdown — tables for comparisons/status, bold verdicts,codefor flags/paths/commands, fenced blocks for output, emoji as a leading status glyph (signal, not decoration), and avoid renderer-dropped syntax. Styling organizes, never pads. SessionStart is the primacy slot. Its companionverify-first-orch.js(also SessionStart) carries the always-on orchestration ruleset (rules A–N + the DevSwarm-Primary workspace-tier rule W; by default inline at SessionStart, seecontext.orchFullOn) — split out in 0.60.0 so both halves clear the ~10k per-hook injection cap and land 100% inline instead of one spilling to a file (Claude Code leaves a 2,000-char preview inline and does not ask Claude to read the rest). - Surviving compaction — SessionStart re-fires after a compaction with
source="compact". The no-matcher SessionStart registration therefore re-injects the protocol across the compaction boundary, exactly when context is largest and adherence is worst. This is the sole compaction-survival mechanism. The hook is deliberately not registered onPreCompact: per the official docs, only UserPromptSubmit / UserPromptExpansion / SessionStart can injectadditionalContext, so a PreCompact hook would deliver nothing. - Per-turn nudge —
verify-first.jsinjects ONE short one-liner per turn (one of 17 facets of the Iron Law), so the per-turn slot stays high-salience instead of being habituated and tuned out. The facet is chosen deterministically by a SHA-1 hash of the entire UserPromptSubmit stdin envelope — which carriessession_id/transcript_path/cwdalongside the prompt. So the nudge is reproducible for a given full envelope, and the same prompt text in a different session or cwd intentionally rotates to a different facet (extra novelty against habituation). Nothing from stdin is echoed back into the injected text.
git-guard¶
git-guard.js (PreToolUse on Bash) mechanically blocks three things:
- Commits whose
-m/--message, or whose-F/--filemessage (a-F -/--file=-/-F /dev/stdinheredoc, or-F <path>naming a real, readable file, relative to a precedingcd), carries aCo-Authored-By/ self-credit trailer (including the canonical emoji-prefixedGenerated with [Claude Code]footer). Commits take no AI credit. An unreadable-Ffile fails open (not scanned) rather than guessing at its contents. git push --force(and quoted/bundled variants). History rewrites are a deliberate human action.- A push that deletes a remote branch or tag (
--delete,-d,origin :<ref>,--prune, including abbreviated long options). Deleting published refs needs explicit owner confirmation; the block names theskip.jsonoverride.
It uses a quote-aware tokenizer that inspects argv positions, so quoted force
flags (git push "--force"), bundled -f, +refspec pushes, and a trailing
--force after a 2>&1 redirect are all caught. It also unwraps bash -c /
sh -c / zsh -c / dash -c / ksh -c / ash -c shell wrappers and re-inspects
the payload, so bash -c "git push --force" and bash -c '...Co-Authored-By:
Claude...' cannot smuggle either block past it that way.
Heredoc bodies that are data (guards.gitGuardHeredocData, default on): a
heredoc whose consumer is not a shell is not scanned as commands, so a note,
commit message or PR body that mentions git push --force is not blocked:
cat <<EOF > notes.md, tee notes.txt <<EOF, git commit -F - <<EOF,
git commit -m "$(cat <<EOF ... EOF)", gh pr create --body-file - <<EOF. The
rule is all-or-nothing and fails closed: it applies only when every heredoc in
the command ends in a prose/data file (.md, .txt, .rst, .log, ...)
through cat/tee or in a git commit/tag/notes/merge or gh pr/issue/release
message; every other command in the line is on a short allowlist (cat, tee,
git, gh, echo, printf, cd, mkdir, wc, head, tail, ls, ...); a git there is
limited to commit/tag/notes/merge, status/log/diff/show/add and rev-parse, each
with its own flag allowlist (no fetch, push, pull, clone, remote, submodule or
config, no -c/--git-dir; any other flag, an abbreviation like
--upload-p included, keeps the bodies scanned), and a gh to pr/issue/release
create/edit/comment with listed flags and no -- passthrough; no write target is a
script, an extensionless file, a dotfile, a git hook name or a .git,
.husky, .githooks, hooks, .ssh, .config, .claude, .codex or
.anti-hall/bin path; an unquoted-delimiter body has no $( or backtick; and
the parse has no open quote, line continuation or process substitution around
the opener. Everything else still scans every body as shell: a heredoc fed to
bash/sh/zsh/eval/source/./xargs/python/node/..., piped into a shell, teed into
>( ), or written to a file that the same command line then runs (bash
f.md, . f.md, git -c core.pager='sh f.md' log). Commit and PR credit
trailers and key = value git-config lines in a body are checked whatever the
consumer. A script written by a heredoc (cat > x.sh <<EOF) is still scanned,
because it may be run later; write it with the Write tool.
tests/hooks/git-guard-heredoc-bypass.test.js pins the forms that must stay
blocked, tests/hooks/git-guard-heredoc-data.test.js the ones that are data.
It scans commit messages both INLINE (-m / --message / --trailer) and via
-F - / --file=- / -F /dev/stdin fed by a heredoc on the same command
line, or via -F <path> naming a real, readable file (a relative path
resolves against a preceding cd in the same command). For any git verb that
writes a commit (commit, merge, rebase, cherry-pick, revert, am,
pull, commit-tree) it also scans the WHOLE command text for a self-credit
trailer line, so a pipe into -F -, a file written earlier in the same command,
a shell variable, or a rebase -x payload is caught; gh pr merge --body /
--subject and gh ... --body-file are scanned too. A PostToolUse audit
(git-guard.js --audit) then reads the commits HEAD gained in the last 15 min
after any commit-creating command and tells the agent to reword one that
carries a trailer added OFF the command line (a repo commit-msg /
prepare-commit-msg hook, a template, an editor, a cherry-pick). Documented
fail-open scope: the audit is advisory (PostToolUse cannot un-run a commit),
a commit made inside a script file is not seen by the command-text scan, and
a shell alias or function defined in your shell profile (not in the command)
is not seen through (see Aliases below). xargs is covered:
... | xargs [options] git ... gets the same git checks as a direct run (any
xargs-run git push is blocked, since stdin can append --force), with
xargs' GNU and BSD options parsed the way getopt does (-I {}, -I{},
-i/-l/-e with attached values only, -n1, -0n1, -d '\n', -J %,
--max-args 1), and an xargs-run sh -c/eval/nested xargs is re-scanned
(its "$@" is scanned with a --force standing in for the stdin words).
find ... -exec|-execdir|-ok|-okdir CMD ... \; (or +) gets the same checks
for CMD, and a find -exec git push ... {} is blocked (a file name can be a
+ref force refspec). Wrapper commands are unwrapped with their own option
grammars, directly and under xargs/find: env, command, exec, sudo, doas, nice,
nohup, time, timeout, stdbuf, caffeinate, ionice, flock (-c/--command
before or after FILE is scanned as sh -c), setsid, chrt and taskset. GNU
parallel is a runner like xargs: the command before :::/:::: gets the
same checks with the ::: words appended (a parallel-run git push is
blocked), and parallel ::: 'cmd' scans each input as a command. When a
replacement string (xargs -I/-i/-J, find's {}, parallel's {}/-I) is
the command word, the git subcommand or an argument before it, the command is
unknown and is blocked if a force or remote-delete flag is visible
(xargs -I{} git {} --force). Custom placeholders (-I{x}, parallel's
{1}/{.}/{/}/{#}) are ordinary words: { is a group brace only as
a standalone word (blanks on both sides, so function f { ...; } and
coproc NAME { ...; } bodies are scanned) and } only after ;, & or
a newline. When
xargs or parallel runs git with no subcommand word, the subcommand comes
from stdin, so it is blocked if a force or remote-delete token appears
anywhere on the same command line, also after quote removal
(echo 'push --force o m' | xargs git, echo push '-'f o m | xargs git).
When the script comes from stdin (xargs -I{} sh -c '{}', | parallel
with no command, | sh, | bash -s), the quoted strings and echo/printf
words on the line are scanned as commands. A sh -c / bash -c script
that forwards its positional args (sh -c '$0 "$@"' git ..., bash -c '"$@"' _
git ...) is scanned with those args spliced in. These are
documented boundaries, not silent gaps.
Aliases (guards.gitAliasResolve, safety, default on). git <name> where
<name> is not a git builtin is resolved through the git config of the repo the
command runs in (one git config --get-regexp ^alias\. per repo per call;
builtins never spawn git, since git ignores an alias that shadows one). Alias
chains are followed (a loop resolves to nothing, as git refuses to run it), and
the expansion is scanned as the command it really is: git <body> <args>, or the
shell text of a ! alias. The PostToolUse audit follows aliases too. Defining
an alias whose body is a blocked git command is itself blocked:
git config alias.x '<body>', git -c alias.x=<body>,
GIT_CONFIG_VALUE_<n>=<body>, and a shell alias x='<body>' (shell function
bodies are ordinary command segments and were already scanned). A call to a
shell alias or function defined in the same command is scanned as what it
forwards: g(){ git "$@"; }; g push --force expands "$@"/$*/$1… to the
call's arguments, wrappers calling wrappers included (depth-bounded). Not
covered: a shell alias or function defined in your shell profile rather than in
the command (the hook only sees the command text).
Reused commit messages (guards.gitReusedMessageCheck, safety, default
on). A git commit with no -m/-F takes its message from somewhere the
command line does not show. Before it runs, git-guard reads that message:
git log -1 --format=%B <rev> for -C/-c/--reuse-message/--reedit-message,
HEAD for --amend, the -t file or commit.template. If it carries an AI
self-credit trailer the commit is blocked when the message would be reused
verbatim (-C, --no-edit, or a no-op editor such as GIT_EDITOR=true), and
also on the editor path when the command sets no real editor of its own. A
command that sets a real editor (GIT_EDITOR=…, core.editor) is left to the
PostToolUse audit, since that editor may be the cleanup. Why both layers: the
PreToolUse check can only see a message that already exists (a commit, a file);
what an editor or a commit-msg hook writes exists only after the commit, so
the audit reads the new HEAD and tells the agent to amend.
Task discipline¶
task-tracker.js(UserPromptSubmit) injects the directive every turn: capture every request as a task before acting, assign priority (P0/P1/P2), keep the list sorted highest-priority-first and work in that order, keep statuses current, delegate heavy work to background subagents, and report progress. Nothing is silently dropped.task-guard.js(Stop, loop-safe) blocks once when the session is about to stop with open tasks (pending/in_progress) still in the list, prompting the model to continue, complete, or explicitly defer them. If the exact same open-task set was already blocked on (nothing changed), it skips to prevent infinite loops. Fail-open on any parse/read/state error.tasklist-guard.js(Stop) blocks when non-trivial work — ≥ANTIHALL_TASKLIST_WORK_THRESHOLD(default 3) file-mutating actions — happened without task tracking (or with more than one taskin_progress, or without a fresh per-session progress file at<cwd>/.anti-hall/progress/<date>/<session-id>.md(<date>= UTCYYYY-MM-DD,<session-id>= the sanitized Claude Code session id) — collision-free across concurrent sessions on the same project, replacing the old single shared.anti-hall-progress.md. Writes that land only under the session's own scratchpad (inter-agent message files, temp scripts that write nowhere else) do not count as work, and a write to the progress file seen in the transcript counts as fresh even before its mtime is visible (v0.107.0). It coexists withtask-guard(which drains declared tasks) and keeps an independent block cap (MAX_BLOCKS=3cumulative/session) so the two never compound. The progress file lives under.anti-hall/, which is NOT git-ignored automatically in your project: add.anti-hall/to your.gitignore(or rundoctor --repair, which appends it to the repo-local.git/info/exclude;doctorwarns and a once-a-week SessionStart reminder fires while it is not ignored —guards.gitignoreHint). The hook never creates the file, and it must be updated this session (default 30 min freshness window) to count. A running.anti-hall/progress/INDEX.md(and the history-side equivalent) is maintained via atomic single-line appends only — never a read-modify-rewrite. Fully fail-open. SeeTASKLIST-GUARD.md.
User-override escape hatch (skip-guard)¶
The user's explicit instruction outranks any guard. When the user clearly and directly
asks the agent to skip a guard, the agent records that consent via the shared skip-guard.js
primitive — a TTL'd JSON marker at ~/.anti-hall/skip.json, e.g.
{ "tasklist-guard": <unix-ms expiry>, "all": <unix-ms expiry> }. Every guard checks it at
startup and fail-opens while it is in effect; the marker auto-expires (default 15 min) so a
safety guard is never left silently disabled.
- Granular: name a single guard (
"speculation-guard","tasklist-guard","limit-conserve", …) or use"all"to cover the noisy guards at once. - Safe default: a broad
"all"skip does not cover the destructivegit-guard(force-push / AI-credit trailer) — to skip that, the agent must name"git-guard"explicitly. - Fail direction is inverted from the hooks: a missing/corrupt skip file makes
isSkippedreturn false, so the guard stays active. A broken skip file must never silently disable protection.
Several Stop hooks are registered (
task-guard,speculation-guard,speculation-judge,tasklist-guard,codex-nudge), all emitting the top-level{"decision":"block","reason":...}Stop schema. When several fire on the same Stop, all block and Claude Code gives the model each reason as its own message; the engine's dispatcher answers them as one block that carries every reason in registration order.task-guardis registered first because open-task discipline is higher-stakes, so its reason comes first. Each is capped (task-guard caps atMAX_BLOCKS; speculation-guard blocks once per distinct speculative message hash; speculation-judge blocks once per distinct message hash;tasklist-guardhas its own independent block cap —MAX_BLOCKS=3cumulative per session — so it never compounds withtask-guard), so the others surface on subsequent Stops.speculation-judgeis a no-op unlessjev.semanticJudgeis true orANTIHALL_SEMANTIC_JUDGE=1— it never blocks in the default configuration.
speculation-guard¶
speculation-guard.js (Stop) provides lexical enforcement of the no-speculation
Iron Law at the output boundary — after the model has already produced a reply.
How it works:
- Reads
transcript_pathfrom stdin, parses the JSONL, and extracts the last assistant message (all text content blocks concatenated). - Scans for speculation markers (case-insensitive, word-boundary):
very plausibly,plausibly,presumably,I suspect,my guess,I'd guess,I bet,likely,probably,must be,should be(but notshould I),seems to be,appears to be,I think it's,my hunch. - Suppresses the block if the same message also contains an evidence/uncertainty
acknowledgment:
verified,I don't know,haven't checked,not verified,unverified,let me verify,I'll check,I will check,need to confirm,to confirm, afile.ext:linecitation,running,per the data,the data shows. This allows honest hedging ("I haven't checked, but it might be X — let me verify") while blocking silent inference-as-fact. - Block-once / loop-safe: hashes the last message text; stores the blocked hash
in
~/.anti-hall/speculation-guard-state-<session>.json. If the same message hash was already blocked (nothing changed between Stops), skips the block — the model was nudged once and had a chance to respond. Never wedges. - Fail-open: any parse/read/write error exits 0 without blocking or writing to stderr. A bug here never wedges a session.
Unsupported confident inferences (opt-in). The hedge-word scan cannot see a confidently-stated inference that uses no hedge word ("The crash is caused by the cache race."). Two opt-in components cover it, both off by default because neither reached 0.9 precision when measured:
guards.inferenceCheck(deterministic, free, ~ms): finds causal or attributive sentences ("caused by", "the root cause is", "because", "is due to", "stems from", "this means", "the culprit is", "that's why", "comes down to", "Root cause: X", "X is what crashes Y", "theis the ...") and blocks once when nothing in the transcript window mentions the stated cause. Evidence is tool results, the inputs of observation tools (a command, a path, a search pattern), fenced blocks the user pasted and task notifications, across the last 1 MB of the transcript (not only this turn). The assistant's own prose and anything it authored (Write/Edit/apply_patch input) never count. Skipped: questions, conditionals, modal hedges, first-person rationale ("I used X because Y"), plan/next-step lines, quoted or code text, and replies that say the claim is unverified. Works on Claude transcripts and Codex rollouts. Not covered: a claim with no causal wording ("The cache race crashes the worker."), "since" (too often temporal), and a tool call that merely echoes the claim's words (it counts as evidence). - The semantic judge (Tier 3 below), which now also sees the user's request and the tool
evidence, and can run on the local
claudeCLI without an API key.
Measured (node tools/eval/inference-bench.js, 84 synthetic labelled cases, 42 positive / 42
negative, written before the detector):
| Component | Precision | Recall | Notes |
|---|---|---|---|
guards.inferenceCheck |
1.00 | 0.95 | Synthetic corpus, same for Claude and Codex transcript shapes and end to end through the hook (--hook). First untuned run: 0.975 / 0.929. |
guards.inferenceCheck, real replies |
≤ 0.45 (estimate) | not measured | Replay over 3,276 real final replies from local sessions: 3.8% flagged (125). In a random sample of 40 of the 76 unique flagged sentences, at most 18 were causal claims about project state at all; the rest were design rationale ("X because Y"), scheduling notes, test-result reports, hedged lines or general knowledge. |
Semantic judge, cli, Haiku |
0.78–0.81 | 1.00 | Two runs; different false positives each run. The judge demands proof of the causal link that the corpus labels accept. |
Semantic judge, cli, Sonnet |
0.81 | 1.00 | One run; several of its "false positives" are defensible (the claim goes beyond the evidence). |
The corpus is synthetic and written by the same author as the detector, so treat the synthetic numbers as a regression floor, not a field estimate.
Three tiers of anti-speculation enforcement¶
| Tier | Component | On by default | Mechanism | Cost / latency |
|---|---|---|---|---|
| 1 | verify-first-full.js + verify-first-orch.js + verify-first.js |
Always-on | Protocol injection (SessionStart + per-turn nudge): names every rationalization bypass including confident inference-as-fact and hedge-word speculation. | Zero (no API call; text injection only). |
| 2 | speculation-guard.js |
On by default | Lexical Stop hook: scans for 15 hedge-word markers, suppresses when acknowledgment present. Catches hedged speculation. Cannot catch confident inference-as-fact with no hedge word. | Zero (pure Node, no API call). |
| 2b | speculation-guard.js with guards.inferenceCheck |
OPT-IN (off by default) | Deterministic: a causal claim whose stated cause no tool evidence in the transcript window mentions. | Zero (pure Node, no API call). |
| 3 | speculation-judge.js |
OPT-IN (off by default) | Semantic Stop hook: an LLM judge, given the reply, the user's request and the session's tool evidence, decides whether the reply asserts an unverified fact with no hedge word and no acknowledgment. Catches the gap Tier 2 misses. | api backend: ~$0.0001-0.001 per turn + ~1-3 s (estimate), needs anthropic_api_key. cli backend: no key, your Claude login's usage, ~5-6 s (measured). |
speculation-judge (Tier 3, OPT-IN)¶
speculation-judge.js is registered in hooks.json but exits 0 immediately unless it is
enabled: off by default; enabled by the jev.semanticJudge setting or ANTIHALL_SEMANTIC_JUDGE=1
(the env var wins when set to an on/off value). When off, it has zero cost, zero latency, and
zero network activity — it is as if it were not registered at all.
Quick switch: node scripts/settings.js judge on|off|status (or ask /anti-hall:settings) sets jev.semanticJudge, reports whether a key is visible to that process (never the key), and prints the cost (api backend: about $0.0001–0.001 and 1–3 s per turn end, estimated; cli backend: no API bill, about 5–6 s, measured) and the measured precision (0.78–0.81, recall 1.0, on tools/eval/inference-bench.js). It also names the active backend: when jev.enabled is on and the speculation integration is on, speculation-guard already asks Jev (a remote classifier, not a local one) and this API judge exits early. doctor prints an info line with the backend, or this command while the judge is off.
To enable: either set jev.semanticJudge to true (/anti-hall:settings), or:
# Add to ~/.zshrc / ~/.bashrc / ~/.profile, then restart Claude Code:
export ANTIHALL_SEMANTIC_JUDGE=1
Then store the key in the plugin's options screen (anti-hall -> anthropic_api_key, kept in the OS
credential store; the judge is fail-open if it is absent), or use your own Claude login
instead of a key: node scripts/settings.js set jev.judgeBackend cli (auto = the key when one
is visible, else the CLI). The cli backend runs claude -p with no tools, no MCP servers, no
settings files and every hook disabled (--settings '{"disableAllHooks":true}'), so the judge call
cannot run anti-hall's hooks or recurse; it also sets ANTIHALL_JUDGE_CHILD=1, which makes the
judge exit at once if it ever runs inside that child. Measured about 5–6 s per turn end
(claude 2.1.288, Haiku). If claude is not on the hook's PATH, or is not logged in, the judge
does nothing. anti-hall no longer reads
ANTHROPIC_API_KEY from your environment unless you enable guards.allowAnthropicEnvKey in
~/.anti-hall/settings.json (the Codex port, which has no plugin options, needs that setting).
To disable: set jev.semanticJudge to false and unset ANTIHALL_SEMANTIC_JUDGE (an explicit ANTIHALL_SEMANTIC_JUDGE=0 overrides a true setting).
What it catches: confidently-stated inference-as-fact with no hedge word — e.g., "The cause is the old build artifact." with no tool verification and no uncertainty acknowledgment. The judge sees the latest user request and up to ~6 KB of the newest tool evidence from the transcript (secret-scrubbed), so a claim the session's tool output shows is allowed. The judge prompt instructs the model to ALLOW honest hedging, quoted text, hypotheticals, plans, and general software knowledge; it only blocks definitive unverified factual claims.
Fail-open: any error (absent anthropic_api_key, API unavailable, timeout, bad
JSON response) exits 0 without blocking. A failure here never wedges a session.
Where a stored key is visible. A key stored through the plugin options (jev_vercel_api_key,
jev_typesafe_api_key, legacy jev_api_key, anthropic_api_key) reaches hook processes and the workers they spawn only; Claude
Code does not hand it to the Bash tool, the statusline, monitors or the companion daemons.
So jev-setup test/status, jev-report (credit balance) and finding-dedup print a
one-line "no Jev key visible to this process" reason when run from a shell. To make a key
available to those background tools (and to Codex), save it with jev-setup set-key and
enable jev.allowLegacyKeyRead in ~/.anti-hall/settings.json (a safety setting: it takes
--confirmed, and env or project settings cannot flip it).
Loop-safe: hashes the last message text (with a ":judge" suffix to keep the
namespace separate from speculation-guard's hashes). If the same message hash was
already blocked, skips — the model was nudged once and had a chance to respond.
Misfire caveat: LLM judges are not perfect. The conservative judge prompt reduces
false positives, but some misfires will occur — particularly on messages that describe
what code does based on reading it (which IS verified by inspection). If misfires are
frequent in your workflow, turn the semantic judge off (jev.semanticJudge false, ANTIHALL_SEMANTIC_JUDGE unset) and rely on Tiers 1 + 2.
Cost and latency detail: one model call per Stop event when enabled, to jev.judgeModel
(default the haiku alias, which resolves to the latest Haiku; env-overridable via ANTIHALL_JUDGE_MODEL).
With jev.judgeBackend cli the engine can make this call itself (engine/defaults/judge.toml) in a one-shot
ah-engine check speculation-judge, leaving a row (backend, model, latency, error) in
~/.anti-hall/logs/judge-calls.ndjson; the resident daemon cannot wait seconds for a model, so there it leaves the call to
the Node hook.
At current Haiku pricing this is roughly $0.0001-0.001 per turn; latency is roughly
1-3 s added to each Stop (an estimate: the API backend has not been timed or
evaluated). The keyless cli backend is measured: precision 0.78-0.81, recall 1.0 on
tools/eval/inference-bench.js, about 5-6 s per Stop. For projects where confident inference-as-fact is the primary
failure mode and the cost/latency is acceptable, Tier 3 closes the gap Tier 2 leaves open.
Jev classifier (opt-in, backs Tier 2)¶
speculation-guard.js can optionally ask Jev (TypeSafe's "System One" model — a
typed yes/no classifier, not a reasoning model) first. Default OFF. Enable with
~/.anti-hall/jev.json:
Text sent to the gateway (prompt, last assistant message, commit/PR text, test output) is passed through a best-effort redactor first: text matching known token shapes (API keys, Bearer tokens, password= style assignments, PEM blocks, JWTs, URL credentials, emails, long token-like runs) is replaced with [REDACTED...] placeholders. Redaction is best-effort, not a guarantee.
or ANTIHALL_JEV=1 (env). ANTIHALL_JEV=0 always force-disables. Only a confident
"speculative" answer blocks on Jev's word alone; a "grounded" answer, low confidence, or
any Jev failure (no key, timeout, HTTP error, bad response) falls back to the regex check
unchanged. Disabled (the default), the guard behaves exactly as the regex-only hook.
Details: docs/KB-jev-classifier.md.
Two more integrations share the same opt-in switch via hooks/lib/jev-assist.js, each
with its own on/shadow/off mode and trust rule: speculation-judge.js skips its paid
Haiku call when speculation is fully trusted ("on", not "shadow"), and
model-routing-guard.js (default mode shadow) can let a confident non-mechanical
classification downgrade a mechanical-flagship block to an advisory. Run
node plugins/anti-hall/scripts/jev-report.js for a per-integration KEEP/REVIEW/REMOVE
read on whether any of this is worth trusting. Details: docs/KB-jev-classifier.md §10.
Skills reference (detailed)¶
Moved from plugins/anti-hall/README.md "Skills" (v0.107.0 doc sweep).
Always-on vs conditional. The root-cause and orchestration disciplines are
enforced always-on via the hook layer — their core fires every session/turn through
verify-first-full.js + verify-first-orch.js (SessionStart) and verify-first.js
(per-turn nudge), so they apply without being invoked. The full step-by-step playbooks below are still available as
slash commands for when you want the deep version. deadly-loop and ship-it
are conditional skills invoked on match — they are not forced every turn. The
always-on orchestration injection enforces a bias toward delegation — default to a
subagent for any work that touches files/tools/commands/search/build/test or could
balloon (to avoid the eager "I'll just do it inline" trap that pollutes the main thread),
handling inline only genuinely atomic things (a direct answer, a single known-line read,
the coordinator's own synthesis/decisions), and delegating immediately if a quick inline
task balloons; parallel agents when independent; commands via Haiku off-thread. It now
also defaults delegated heavy/parallel work to the background — the coordinator passes
run_in_background itself so the user needn't background it manually, while still
verifying each on completion (never fire-and-forget). It also
enforces verify delegated work — a subagent's "done/passing" is an unverified claim
re-checked against ground truth (re-run the authoritative check, or use a separate
verifier, reconciling multiple workers against ground truth) before marking complete —
capture-every-request task discipline (priority-sorted),
anti-sycophancy (challenge a wrong premise with evidence; user agreement is not
correctness), and scope & fidelity (solve the actual problem with the simplest
sufficient solution; intent over letter; confirm before expanding scope; match rigor to
blast radius; finish what was asked and drop nothing).
Invoke via slash command:
/anti-hall:root-cause— evidence-driven debugging: reproduce, collect evidence, instrument when missing, trace the sequence to the original + root cause (not the surface symptom), prove the hypothesis, fix at the root, verify./anti-hall:orchestration— swarm with a non-blocking main thread: delegate heavy/long work to background + parallel subagents, partition to avoid conflicts, distribute load across Claude and Codex when available, run commands via Haiku so raw output never pollutes the coordinator's context./anti-hall:ship-it— one lean workflow for shipping any change, scaled S/M/L to blast radius: brainstorm + plan in plan mode (ExitPlanMode is the approval gate; blends superpowers planning ideas — standalone, no external dependency), enumerate edge cases, harden the plan with the deadly-loop BEFORE any code, fan large work out as a Workflow swarm, and verify each phase with fresh evidence + a vacuous-test guard, running the deadly-loop after each phase until zero NEW P0/P1s. L tier adds a resumable.anti-hall/ship-it/<slug>/STATE.json(plan hash + per-phase status + an escalation counter capped at 2 build→re-plan loops), logs accepted P2 findings todecisions.md, routes build seats Codex-primary with Sonnet failover (a cross-model guard skips the Sonnet Reviewer when a phase's build fell back to Sonnet, to avoid same-model self-review), and closes out with a session-history entry +SUMMARY.md. v0.67.0: the per-phase gate previously ignored dead review seats entirely, so fewer live seats produced fewer findings and a silently PASSINGconverged: true— missing review coverage is no longer indistinguishable from a clean pass. The gate result now carriestotalSeats/liveSeats/deadSeats/degraded/seatReports, andconvergedrequiresdeadSeats === 0— a phase that loses a seat now correctly fails to converge where it previously passed silently. Also honorsargs.codexAvailable(mirroring deadly-loop, including the Opus adversarial-persona fallback)./anti-hall:deadly-loop— iterative parallel Reviewer + Critic debate + fix-waves until convergence (zero NEW P0/P1s). The debate engine behind ship-it's gates. On convergence, writes an ADVISORY~/.anti-hall/approvals/<repo>@<HEAD-sha>.jsonrecord ("proof": false— not authorization; a real gate must still enforce its own check)./anti-hall:deadly-loop-multi— scaled-up deadly-loop: N Reviewer + N Critic pairs with diversified lenses, then dedup + synthesize (double / triple / quadruple)./anti-hall:install-statusline— writes the statusLine setting (global by default, per-project on request) and reminds you to restart.--consolidatemerges with an existing statusline (e.g., OMC HUD) instead of replacing it; base persisted to~/.anti-hall/consolidated-base.json. Env:ANTIHALL_STATUSLINE_BASEpins the base expression explicitly./anti-hall:doctor— health-check: confirms Node is found, every hook is present + syntax-valid, and the guards actually fire (live behavioral self-tests on e.g. git-guard / command-guard / swarm-guard / speculation-guard / tasklist-guard). Also env-aware: detects and tests each optional integration only when it's actually present — OMC (plugin-enabled + live-loop check), Codex/OMX (config/skills detection), and the DevSwarm liveness supervisor (supervisor-companion-installed state plus a per-workspace liveness self-test;nudgedreads as WARN, not FAIL) — silent and skipped for any integration that isn't in play. v0.61.0: repair mode also auto-safe-folds every prior DevSwarm mesh-store shape (phantom/dual/subdir-split/ stale) onto one canonical worktree viafoldMeshDuplicates; the dry-run detect pass doubles as a read-only mesh-shape check under--check. v0.65.0: the heartbeat now carries the monitor outcome, so an ingest daemon that is alive but failing every cycle (e.g. a permanent ENOENT/EACCES/ENOTDIR config fault) is reported as a FAILURE rather than healthy, with a one-line in-session banner; a heartbeat missing these fields (pre-upgrade daemon) reads as unknown, never a fault. New explicit, opt-in--reclaim-ingest-locksweeps orphaned locks and reclaims a contended one (positive OS confirmation required before any removal), then reinstalls — never runs automatically. Install-time also detects a memory-guard/reaper script that would kill the service-managed daemon and reports the exact allowlist entry to add. v0.104.0:--check/repair mode also runs a read-onlyidentity-rekey-candidatesreport, listing stores written under a pre-0.104.0 wrong project key with their message counts; nothing is moved automatically./anti-hall:update— updates anti-hall in place:git pull --ff-onlythe marketplace clone, syncs the version-pinned cache (semver-anchored, traversal-proof), prints the changelog delta between installed and latest, then instructs/reload-pluginsfor in-session reload (field-verified 2026-10-01, also after a harness registry change; restart Claude Code only if a hook or skill path still shows the old version, or to re-run SessionStart-only injections). Hooks and statusline pick up from disk immediately;/reload-pluginsrefreshes the skill list and version label.--checkmode answers "is anti-hall up to date?" without pulling or writing. After a pull, also runsscripts/migrate-state.jsonce per repo (idempotent) to fold legacy root.anti-hall-progress.md/.anti-hall-history.mdinto.anti-hall/history/legacy/, runs the samefoldMeshDuplicatesDevSwarm mesh-store migration doctor's repair uses (v0.61.0), then a dynamic capability scan (scripts/capability-scan.js, read-only) reports each opt-in capability shipped in this build (companions discovered fromcompanion/install-*.js, statusline, pending state migrations) as available-vs-active on this machine, with the exact command to enable any gap — never auto-installs./anti-hall:devswarm— explains anti-hall's optional DevSwarm integration: thehivecontrolreference KB, the designed-but-unbuilt workspace-tier orchestration, the shipped layered recovery model (child self-report → supervisor poke → escalate — the automatic path never kills), and the on-demanddevswarm-recoverCLI (the only path that ever kills), including the full activation checklist and tunable env vars. v0.61.0: also covers the mesh self-heal set — drain-aware routing + phantom-only rescue, theorphans[]/staleRegistryPartitions[]health projection, thediagnose/healthcheckread-only verbs, andfoldMeshDuplicatesmigration./anti-hall:handover— writes a comprehensive, organized, minimal-but-lossless session handover under.anti-hall/handovers/: a global index plus a per-sessionHANDOVER.md(front-loaded, fixed SBAR-derived schema, ≤200 lines) and detail files (state.md/decisions.md/trials.md/knowledge.md), sequence-chained to prior handovers, so a fresh session can resume without re-deriving or guessing anything. Paired with thehandover-resume.jsSessionStart hook, which surfaces the latest handover automatically after/clearor compaction. Codex mirror:codex/skills/anti-hall-handover./anti-hall:defects— new in 0.80.0. File, list, show, and read rulings on anti-hall's own defect reports via the durable, home-scoped, two-way channel that shipped in v0.78.0 (hooks/lib/defect-store.js) but had no discoverable entry point until now — thedefect-nudgehook's runtime pointer led to a skill that did not exist, on both ports, and the channel was documented only in files an agent doesn't load. Five verbs (report/list/show/rule/archive);reportaccepts--sym-file/--repro-fileso a body with backticks,$(...), or embedded newlines never needs shell escaping; a report matching a defect already ruledfixedderivesregressed(build at or past--fixed-in) orstaleBuild(older build) from the reporter's own installed version — no new storage.list --mineidentity now prefers an explicit--projflag, thenANTIHALL_DEFECT_PROJ, then the project's git repo key (was cwd basename only, which silently broke--minefor reports filed from a scratch directory). Thedevswarmskill now points here too, since that's the skill a DevSwarm session actually loads. Codex mirror:codex/skills/anti-hall-defects. Bug history (hooks/lib/defect-history.js):defect.js backfill --repo <path> [--dry-run]imports everyfix:commit from git history once (keyed by sha, so a re-run adds nothing, stored indefects/history/, never inlist --open).defect.js recurring [--since <version|date>] [--top N]groups reported and imported fixes by component and cause and flags hotspots (a component fixed 3+ times, or the same component and cause 2+ times) and likely regressions (the same component and cause fixed again within 5 releases, or an explicit--regression-of).defect.js similar <text…> [--component X]shows the 10 closest past fixes. Theroot-causeskill runssimilarbefore an anti-hall fix. Reports and rulings also accept the optional--component,--causeand--regression-offields.
MODEL-POLICY.md is the shared TRIO roster (Reviewer = Sonnet model:"sonnet" effort xhigh;
Auditor = latest Opus model:"opus" divergent regression/coupling lens effort high;
Critic = Codex latest xhigh reasoning when available, else a divergent Opus adversarial persona). It is
duplicated — see Contributing.
DevSwarm (current state)¶
Optional and dormant unless a DevSwarm session is active (DEVSWARM_REPO_ID) or one of
its opt-in companions (ingest daemon, liveness supervisor) is installed; nothing else in
anti-hall depends on it.
- Mesh only. The Primary and its children talk through anti-hall's per-project store
via
scripts/devswarm.js(send,inbox,roster,mesh read,heartbeat); nativehivecontrolmessaging andSendMessageto a workspace are blocked. A child sends as its workspace id; only the Primary checkout sends asprimary-<hash>. - Primary seat. One Primary per project; a new session adopts the seat when the holder
closed, a live conflict is refused until
devswarm.js primary takeover(checked on every cursor write, under the id lock). - Per-turn visibility. The parent-inbox table (app titles, sidebar order, finish/PR
state, unread,
stale anti-hall <v>) and nags (120 s grace for fresh unread, none for the workspace on screen); Stop gates on unread/unanswered questions for Primary and child. - Recovery never auto-kills. Child self-report → supervisor poke → escalate to the
Primary; only
devswarm-recover.js <id>kills. - The app database is ground truth (read-only, capability-gated): archived/deleted
markers, titles, pins, PRs, session map; screenshot sync (
sync-ui) only as a fallback. - Lifecycle. Auto-archive of proven-done workspaces (default on, DevSwarm ≥ 2.5.3,
always by explicit id). "Done" means every finish gate is set, or the child sent its
structured done-report at its current HEAD and its merge is proven: HEAD contained in the
remote default branch (
gitMergeProof, the same checkgate --set mergedrecords), else, only when git can't decide, amergedgate verified at the current HEAD or the app's merged PR row (with no resolvableorigin/HEAD, only the verified gate:default-branch-unknownblocks); a squash merge needs a manual archive; chat text never counts. A child reports done by runningdevswarm.js done [--summary "..."]once its work is merged (its SessionStart directive tells it to): that sets itsdonegate and sends the Primary one[[ANTIHALL_DONE]]message, and the roster shows itdone/archive-pending, so nobody archives finished workspaces by hand. The Primary is recognised by the app DB'sbuilderType, never by aprimary-<hash>descriptor id. The undo is unarchiving it in the DevSwarm app, and it sticks: the sweep logs the HEAD it archived at and never auto-archives that workspace again at the same HEAD; only a newdoneat a new HEAD makes it eligible again. Deleting archived ones only via owner-approvedprune-archived. - Store hygiene. Message retention (archive then prune old bodies), housekeeping sweeps,
supervisor log rotation; every setting is in the
devswarmsection of/anti-hall:settings. - Capability gate. Every
hivecontrolverb / app-DB column is gated by version + detection, default-deny for unregistered invocations; doctor names what is dormant.
Reference: KB-devswarm-hivecontrol.md,
KB-devswarm-app-db.md, the devswarm skill. How it got here
(v0.54–v0.107, dated): archive/devswarm-layered-recovery-history.md.
Contributing / testing (plugin)¶
Moved from plugins/anti-hall/README.md "Contributing" (v0.107.0 doc sweep).
Contributing¶
- Keep the 2 MODEL-POLICY.md copies in sync. The TRIO roster file is duplicated
(
skills/MODEL-POLICY.mdplus a copy underskills/deadly-loop/references/) because skill bundling requires the skill to carry its ownreferences/copy and symlinks are stripped on install. Update both together — they must stay byte-identical. - Bump the version on any behavioral change.
plugin.jsonversionis the sole authority (the marketplace entry carries noversion); without a bump, installed users do not receive the update. Add aCHANGELOG.mdentry. - Keep hooks pure Node (built-ins only) and fail-open, so they run unchanged on macOS and Linux (CI-tested) and never wedge a turn. Windows is not supported (v0.69.0 dropped it from the CI matrix); avoid POSIX-only calls regardless, since pure-Node code may still work there.
Recommended optional: oh-my-claudecode (OMC)¶
oh-my-claudecode is a recommended optional dependency. anti-hall installs and runs fully standalone without it. Two features gain automatic behavior when OMC is installed:
limit-conserveauto mode —limit-conserve-inject.jsreads the OMC usage cache (~/.anti-hall/omc-usage-cache.json) to detect the live context percentage. Without OMC, the hook operates in manualon/offmode only. Account-aware: if the logged-in Claude account changes and the usage cache hasn't refreshed under the new account yet, conservation mode deactivates rather than apply a stale reading across accounts. Kill-switch:ANTIHALL_LIMIT_ACCOUNT_CHECK=off.- Consolidated statusline —
install-statusline --consolidatemerges the anti-hall bar with the OMC HUD. The version chip in consolidated mode reads OMC session state.
Without OMC, both features fall back gracefully (limit-conserve manual only; consolidated
mode still works but requires ANTIHALL_STATUSLINE_BASE to specify the base). No errors,
no breaking change.
Opt-in companion: mcp-reaper (macOS + Linux)¶
companion/mcp-reaper.js is an opt-in interval companion (not a hook) that kills
orphaned MCP-server processes — ones leaked when their spawner (a Claude / codex /
npm / node session) exited without cleaning them up. Install with
node companion/install-reaper.js (macOS → 60 s LaunchAgent; Linux → systemd --user
timer, cron fallback); remove with --uninstall. Safety invariant: a process is
reaped only if its command matches a generic MCP signature and its parent is a
reaper/init (launchd / init / systemd --user) — because Unix reparents a dead process's
children, a live MCP always has a live spawner as parent, so killing an in-use server is
impossible by construction. Recognizes Python MCPs too (uvx/uv + underscore
mcp_server_* forms). Limitation: an MCP run as a LaunchAgent / systemd --user
unit / OS service shares init as a parent (like a leaked orphan) and can be reaped —
exclude it via ANTIHALL_REAPER_EXCLUDE='name|name'. Env knobs: MCP_REAP_DRYRUN=1,
MCP_REAP_GRACE, ANTIHALL_REAPER_MATCH, ANTIHALL_REAPER_EXCLUDE.
Windows is not supported. See plugins/anti-hall/companion/README.md.
Meeseeks supervision — step plans, straying warnings, token burn (v0.117.0)¶
A child workspace is supervised against the brief it was given, not only for liveness. Every piece is additive: a workspace with no plan renders and behaves exactly as before.
- Step plan.
spawn -pturns a numbered list in the brief ("1. … 2. …") into~/.anti-hall/devswarm/plans/<key>.json; aScope: glob, globline becomes the plan's file scope. A brief without a list is never refused.plan set <id> --steps "1. …\n2. …" [--scope glob,glob]writes one later;plan show <id>prints it. The child reports progress withheartbeat <id> --step N --status doing|done|blocked. The workspace table and roster show3/7 done · doing #4 · 42m · progress 18m ago · 1.8M tok. The headline is the COUNT of done steps (several in flight show as3 doing; never a last-touched step index, which jumps for a parallel child); a re-opened step or replaced plan shows the true lower count with(plan changed). Settings:devswarm.planTracking(on),devswarm.planRequired(off: ask children without a plan to write one). - Straying signals (supervisor sweep, plan rows only, advisory, never a kill):
stall— busy with no step progress fordevswarm.stepStallMin(30) minutes;off-scope—ready-check --allow <scope ∪ extras>finds committed files outside the plan's scope;idle— the liveness verdict is stale/nudged/escalated;burn— more thandevswarm.burnTokensWarn(2M) weighted tokens since the step last moved (input + output + cache writes +devswarm.burnCacheReadPct% of cache reads, 10% by default, read incrementally from the child's own session transcript). Each episode warns once; new episodes on the same step repeat up todevswarm.strayWarnMax(2; 0 = off). - What the Primary sees. One capped advisory
DEVSWARM STRAYING: <title>: step N <reason> (Jev: <verdict> <confidence>)line on its Stop (shown once per warning per session, stable alert kinddevswarm-straying, never a block),STRAYING: <signals>and+N extrason the table's finish cell, andplan.straying/plan.tokens/plan.extrason the roster row. - Correction.
correct <id> [--dry-run](Primary, seat-gated) sends "step N '': . Return to step N or reply BLOCKED " as a mesh direct and records warned_at(the stall clock restarts from it) only after the send succeeds. Never automatic. - Respawn (Primary-run, never automatic).
respawn <id> [--dry-run]refuses unless the caller holds the Primary seat, the plan haswarned_at(acorrectwas sent) anddevswarm.respawnGraceMin(20) minutes have passed since it. Then: (a) it asks the child to commit and push its WIP and waits up todevswarm.respawnWipWaitSec(120 s); (b) anything still dirty or unpushed is committed through a private index onto a newpark/<branch>-<ts>branch and pushed — the child's worktree, index and branch are untouched, nothing is stashed or discarded, and a failed park or push aborts the respawn; (c) it writesplans/<id>.handover.md(steps done, remaining steps, last summary, scope, extras, park branch); (d) it spawns<branch>-r<N>with-s <default branch>(never-s <old branch>, which would make the old branch the merge target), step 1 merging the old or park branch, then the remaining steps, carrying scope and extras; (e) it archives the old id and asks the owner to close its app tab.--dry-runprints the plan and changes nothing. Respawn never kills;devswarm-recover.jsstays the only kill path. Plan-file writes (child verbs,correct,respawn, the sweep) are serialized under one lock, so no step update is lost. - Extra work the user asked for. The child records it with
scope add <id> --glob '<paths>' --note '<what the user asked>'(idempotent; the child-turn hook tells a planned child to do this). Tagged globs stop counting as off-scope; the Primary sees the note and can challenge it. - Jev recommendations (
jevIntegrations.devswarmOnBrief,devswarmExtraSanctioned,devswarmWaitKind,devswarmLoop,devswarmStepMap; defaulton= recommendation,shadow= logged only,off): asked detached from the sweep only when a deterministic precondition fires, answer read from the cache on the next sweep. The verdict and confidence ride on the warning as a recommendation; Jev never suppresses a warning, never blocks, never kills. The Primary makes the final call. - Effectiveness.
~/.anti-hall/logs/devswarm-supervision.ndjson(bounded, 1 MB × 5, daily rollups indevswarm-supervision-daily/): warnings by signal, repeats, corrections followed by step progress withinstepStallMin, extras tagged, time-to-done and steps done vs planned, tokens per workspace and per step, burn warnings and their corrected rate, per Jev integration its agreement with the deterministic signal plus how often the Primary followed or overrode it, and respawns (WIP parked or not, aborted, time to first step progress in the new workspace, finished).supervision-report [--days N] [--json]prints it;doctoradds a one-line 7-day summary.
Opt-in companion: DevSwarm layered recovery (macOS + Linux; Windows is not supported)¶
companion/devswarm-supervisor.js is a second opt-in interval companion (not a
hook) — a workaround for claude-code#39755, where a claude session can silently wedge
(process alive, listener dead) with no upstream headless recovery. It is OPTIONAL,
exactly like the OMC/OMX integration: dormant with zero effect unless DevSwarm is
actually in use, gated by hooks/lib/devswarm-detect.js (modeled on omc-detect.js)
and the presence of published workspace descriptors under
~/.anti-hall/devswarm/workspaces/*.json.
The seam: anti-hall ships only the generic supervisor. A DevSwarm-aware consumer
publishes the workspace descriptor (id, worktreePath, sessionId, inboxPath,
cursorPath, optional nudgeCommand/escalateCommand); anti-hall never assumes
DevSwarm's internals beyond that JSON shape.
Three escalating layers, and the automatic path never kills:
1. Child self-report — hooks/devswarm-child-role.js (SessionStart, child-workspace
only) reminds an idle child to proactively message its parent via hivecontrol
workspace message-parent.
2. Supervisor poke — each sweep computes liveness from outbound activity only
(the session's own transcript mtime + git/worktree commit activity — both must be
idle, plus a pending unread backlog, before a workspace is nominated stale); on
stale, it fires the descriptor's optional nudgeCommand and persists verdict
nudged.
3. Escalate-to-parent — once the poke budget (ANTIHALL_DEVSWARM_NUDGE_MAX_ATTEMPTS)
is exhausted, it persists a terminal escalated verdict and fires the optional
escalateCommand. Nothing above ever resolves a pid or sends a signal.
Install with node companion/install-devswarm-supervisor.js (--uninstall to remove,
--dry-run to preview). macOS → LaunchAgent; Linux → systemd --user timer (cron
fallback); default sweep interval 90 s (ANTIHALL_DEVSWARM_INTERVAL, clamped 60-120).
Env knobs: ANTIHALL_DEVSWARM_SUPERVISOR (off/on/auto, default auto),
DISABLE_ANTIHALL_DEVSWARM=1 (hard kill-switch). Sweep thresholds are also env-tunable
(all seconds; invalid/absent falls back to the default, clamped):
ANTIHALL_DEVSWARM_IDLE_SEC (default 900, min 60), ANTIHALL_DEVSWARM_COOLDOWN_SEC
(default 600, min 0), ANTIHALL_DEVSWARM_NUDGE_MAX_ATTEMPTS (default 2, clamped
1–20), ANTIHALL_DEVSWARM_NUDGE_WINDOW_SEC (default 180, min 1),
ANTIHALL_DEVSWARM_NUDGE_COOLDOWN_SEC (default 120, min 0). doctor.js runs a
matching per-workspace check that stays silent unless DevSwarm is active; a nudged
verdict reads as WARN (no more stuck-timer/FAIL check — the automatic path never kills,
so there's no kill-then-resume window to watch for being "stuck"). v0.66.0: doctor no
longer reaps a unit as "confirmed running and healthy" from a weaker second health check
that omitted the pid guard and the monitor-fault check — there is now one daemonHealth
definition, used by every consumer including this reaper.
Message retention (v0.108.0). Each supervisor sweep also runs message retention on
one store, so the DevSwarm stores stop growing without limit. Retention archives old
message bodies to ~/.anti-hall/devswarm/archive/<store>/<yyyy-mm>.ndjson.gz and then
clears them. Rows, read positions and hashes stay, so no unread count changes.
- Only bodies that every reader has already read are pruned.
- The newest 200 rows of each partition, open questions, and each workspace's latest heartbeat are never pruned.
- A store over 100 MB is pruned oldest first until it is under the limit. If the rest is
unread,
doctorWARNs instead. - On a new machine, the first run is a dry-run report (
retention-dry-run.json). - Settings:
devswarm.retention.days(30, 0 = off),maxStoreMB(100),keepPerPartition(200),archive(true) andarchiveMaxMB(0 = never evict;doctorwarns past 500 MB). Pruning is automatic after the first-run dry-run report. Set them in~/.anti-hall/settings.jsonor with theANTIHALL_DEVSWARM_RETENTION_*env vars. - Commands:
devswarm.js retention status | run [--dry-run] [--store X] | restore --store X --month yyyy-mm.
Full rules: KB §8.7, Message retention.
On-demand kill: companion/devswarm-recover.js <workspace-id> — the ONLY path in
DevSwarm that ever kills a process, invoked explicitly per workspace (e.g. on an
escalated verdict). Precise targeted kill: identity-bound (worktree + session uuid),
abstains on any ambiguity (0 or >1 candidates), re-confirms identity on fresh data
immediately before each signal (a pid recycled mid-grace is never SIGKILLed), signals
the process group (not just the pid) so orphaned MCP children are cleaned up too,
and — unlike the automatic path — targets an interactive claude session too, not
just headless (naming the id on the command line is the deliberate override). Capped at
ANTIHALL_DEVSWARM_MAX_RECOVERIES (default 3, clamped 1–20) auto-recoveries before
escalating instead of restart-looping; ANTIHALL_DEVSWARM_GRACE_SEC (default 5,
clamped 1–60) is the SIGTERM→SIGKILL grace window. Windows is not supported.
Codex / cross-tool¶
AGENTS.md is a prose mirror of the verify-first Iron Law + commit hygiene + task
discipline, so Codex agents inherit the same discipline. It lives at the marketplace repo root,
NOT inside plugins/anti-hall/, so it ships only to people who clone this repo — a
/plugin install does not bundle it. Installed users who also run Codex must copy it
into their own repo root manually.
On Codex 0.134 or later, edit-guard, api-guard and ship-it-guard's existence gate
run on apply_patch edits. Shell writes reach the same checks through Bash on both
hosts: command-guard applies edit-guard's verdict (guards.bashEditParity), and
api-guard and ship-it-guard run on Bash too (guards.shellWriteChecks); see the
Codex parity notes. Main thread vs subagent
is detected from the Codex payload itself (turn_id + model, no agent_id), so
command-guard's main-thread heavy-command gate also applies on the Codex main
thread. A Codex started from inside a Claude Code session inherits
CLAUDE_CODE_ENTRYPOINT and is treated as a worker.
Hook reference — plugin "Features" table (detailed, per-hook)¶
Moved from plugins/anti-hall/README.md "Features" (v0.107.0 doc sweep).
Features¶
| Component | Event | Purpose |
|---|---|---|
verify-first-full.js |
SessionStart | The verify-first FOUNDATION: full Iron-Law + rationalization-table protocol, the always-on scope & fidelity discipline (simplest sufficient solution; intent over letter; confirm before expanding scope; match rigor to blast radius; finish what was asked / drop nothing), and the always-vs-conditional skill/disciplines index; survives compaction. |
verify-first-orch.js |
SessionStart | (Default: the full rules inline here. Experimental context.orchFullOn=spawn sends ORCH_COMPACT plus a per-session marker so orch-on-spawn.js delivers the full rules on the first spawn instead. The compact core of verify-first-full.js points at PROTOCOL.md, the generated full text.) The companion to verify-first-full.js carrying the always-on orchestration discipline ruleset (rules A–N + the DevSwarm-Primary workspace-tier rule W). SPLIT from verify-first-full.js in 0.60.0 because the combined ~15.3k-char payload exceeded the ~10k per-hook injection cap — over which Claude Code spills the overflow to a file instead of delivering it inline, so only ~2k chars landed and rules A–N + rule W reached no session inline. Each half is now under the cap; zero content dropped. Survives compaction. |
verify-first-subagent.js |
SubagentStart | Re-injects the compact core (Iron Law, rationalization table, rules, scope-fidelity) plus a short WORKER block into each spawned subagent; context.protocolLevel=full restores today's text (core + DISCIPLINES + teammate note). Deliberately omits the orchestration/delegate block (subagents are workers; re-injecting it would recreate deep nesting). Shared core extracted to verify-first-core.js. |
verify-first-core.js |
Shared module (not a hook) | Single source of truth for every protocol text: today's full text (CORE_FULL, ORCH_FULL, the subagent block), the compact forms (CORE_COMPACT_BODY / coreCompactSession(), orchCompact(), WORKER) and the generator input for PROTOCOL.md. Shared by verify-first-full.js, verify-first-orch.js, verify-first-subagent.js, orch-on-spawn.js and tools/gen-protocol.js. |
orch-on-spawn.js |
PreToolUse (Agent|Task|Workflow on Claude; spawn_agent on Codex) |
Experimental, opt-in (context.orchFullOn=spawn); silent by default. Sends the full orchestration rules (A-N) once per context epoch on the coordinator's first spawn, when verify-first-orch.js sent only the compact lines and left a pending marker (~/.anti-hall/orch-full/). One sender per epoch (O_EXCL claim), a retry slot after a 2-minute lease if no delivered copy shows in the transcript; silent for subagents. Codex: opt-in via context.codexOrchFullOn=spawn (experimental, matcher spawn_agent, sends the Codex wording); otherwise Codex gets the full text at SessionStart. Unverified live (landing point, transcript shape, Workflow spawns). Obeys context.verifyFirstOrchestration and the orch-on-spawn skip name. |
verify-first.js |
UserPromptSubmit | Short, varying one-line nudge each turn (anti-habituation). |
git-guard.js |
PreToolUse (Bash) | Blocks AI self-credit attribution — in git commit trailers AND in gh pr/issue/release create\|edit\|comment --body/--title (the 🤖 footer, Co-Authored-By, claude.com/claude-code link) — plus git push --force and remote branch/tag deletion (--delete, -d, :ref, --prune). Inline values only (--body-file is fail-open). |
api-guard.js |
PreToolUse (Write/Edit/MultiEdit, and Bash) | Blocks code that references a non-existent stdlib/builtin API — resolves module.attr in the code-to-be-written against the installed python3/node and refuses the write when the attribute is fabricated. The mechanical answer to API hallucination. Default = stdlib/builtins (import-safe); opt-in ANTIHALL_API_GUARD_THIRDPARTY=1 also checks installed 3rd-party packages (off by default — verifying a package imports it, running its code at edit time). 0 FP + full in-scope catch on tools/eval/api-guard-bench.js; never probes local/relative modules; fail-open; skip-hatch. On Bash (guards.shellWriteChecks) it checks the text a shell write puts in a .py/.js/.ts file when the command shows it (heredoc into cat/tee, echo/printf); cp, sed -i or python -c writes carry no visible text and are not checked. A write to /tmp or the scratchpad is checked the same way as the Write tool: api-guard judges the code's text wherever it lands (only ship-it-guard's path gate excludes scratch targets), which is intended. Commands over 64 KB are scanned by their heredoc header and body rather than skipped. |
command-guard.js |
PreToolUse (Bash) | Keeps the coordinator clean — blocks heavy commands inline, pushes them to subagents. Subagent-aware via payload, per-segment (quote-aware). v0.76.0: all four DevSwarm destructive-verb blocks now also match the devswarm alias, not just hivecontrol — hivecontrol is a thin shim that execs devswarm (the primary command name, equally on PATH), so running devswarm workspace monitor/read-messages/message-child/message-parent directly used to bypass every block; fixed with a shared verb set and a (?:hivecontrol|devswarm) alternation (latent since the blocks were added, not a new regression; shared file, so both ports are covered). Under a DevSwarm-active session it also redirects destructive native inbox reads (all contexts, own skip devswarm-read-guard): hivecontrol workspace monitor blocks unconditionally, read-messages blocks only with durable-inbox evidence (ANTIHALL_DEVSWARM_INBOX_CMD or a workspace descriptor inboxPath); quoted DATA mentions are not false-positives. Per-project command allowlist (owner-approved 2026-09-26): a repo declares its own sanctioned exact commands in <repo-toplevel>/.anti-hall/command-allow.json ({"patterns":["^anchored regex$", ...]}) — e.g. its own deploy script, which the project's rule says must never be delegated. Runs inline in the MAIN THREAD ONLY (subagents never reach this carve-out — already past the coordinator-only gate). The file applies only after the user TRUSTS its exact content — node scripts/settings.js trust-command-allow [<repo>] --confirmed records the sha256 of the file bytes in ~/.anti-hall/trusted-command-allow.json (outside every repo, keyed by the repo's real path); any edit makes it untrusted until re-trusted, and a symlinked file or .anti-hall dir is refused — so a cloned repo cannot authorize itself. Every pattern must start with ^ followed by a literal command word and end with $, with no unbounded wildcard (.*, .+, [^x]*, [\s\S]+) and no top-level |; anything else is ignored (not just non-matching) and reported by doctor with its reason. The WHOLE command must be exactly one unbroken segment (no ;/&&/||/|/&, no subshell/group, no backtick or $( ) command substitution — reuses the guard's own splitSegmentsDetailed) and carry no unquoted </> redirect and no $, backtick, backslash or <(/>( anywhere (quoted or not), or it never qualifies regardless of the pattern. A qualifying match writes ONE audit line to ~/.anti-hall/logs/command-allow.ndjson ({ts, cwd, repo, pattern, command}). Default config is empty (no repo behavior change until it opts in). Kill-switch: guards.projectCommandAllow (default true). Allow plain push (owner-approved 2026-09-26): in the MAIN THREAD ONLY, git add/git commit/a plain git push [-u|--set-upstream] [remote] [ref] (-u/--set-upstream is the only added flag and needs BOTH an explicit remote and an explicit ref; remote omitted or a configured remote NAME from git remote — a path/URL destination never qualifies, fail-closed; ref omitted, HEAD, or the current branch only — resolved fresh via git symbolic-ref --short HEAD, fail-closed if unresolvable), plus &&/; chains made up only of those three, run inline instead of being delegated. --force/-f/--force-with-lease/--force-if-includes/--mirror/--delete/-d/--all/--tags/+refspec/src:dst to another branch, any other chained segment, and pipes/redirects/subshells stay exactly as blocked as before — except three shapes: a HEAD:<current> / HEAD:refs/heads/<current> refspec (same destination as the bare form), a trailing 2>&1, and ONE final | tail [-n] N / | head [-n] N output filter. git-guard.js's own independent force-push/AI-credit checks are untouched. Kill-switch: guards.allowPlainPush (default true). |
output-verify-guard.js |
PostToolUse (Bash, advisory) | v0.69.0, Harness Phase-1. Scans a completed Bash call's own stdout/stderr for common test/build-runner signatures (jest/vitest/pytest/go test/npm run build/tsc) and flags when BOTH a passing signal ("8 passed", "PASS", "ok") and a failing signal ("2 failed", "FAIL", a confirmed non-zero exit) appear in the SAME run — the shape of a partial-pass summary easy to mis-report as a clean "tests pass". Fail-open on any shape surprise (the exact PostToolUse Bash tool_response field shape is undocumented); never blocks. |
failure-root-cause-nudge.js |
PostToolUseFailure (Bash, advisory) | v0.69.0, Harness Phase-1. Fires when a Bash tool call fails (non-zero exit/tool-level error); injects one short reminder pointing at /anti-hall:root-cause — deliberately terse since OMC already injects its own root-cause reminders in this harness. Stays silent on expected exit-1 predicates (grep no-match, test, diff, git diff --quiet, command -v), on harness refusals, and after the first nudge in a turn (guards.failureNudgeFilter). Fail-open always; off-switch ANTIHALL_FAILURE_ROOT_CAUSE_NUDGE=off; skip-guard hatch failure-root-cause-nudge. |
coordinator-work-guard.js |
PreToolUse (Bash), PostToolUse (Bash) | Main thread only, Claude host only. Counts successful WORK Bash calls over guards.coordinatorWorkWindowMinutes (default 10). One advisory COORDINATOR DRIFT note when the count reaches guards.coordinatorWorkNudgeAt (4), and a block for the guards.coordinatorWorkBlockAtth call (7). The block is the enforcement; the PostToolUse note was live-observed delivering on Claude Code CLI 2.1.238 and is not doc-confirmed; re-verify after a CLI upgrade; blocks are the enforcement. Skip key coordinator-work-guard; also off when safety.commandGuard is off or command-guard is skipped. Fail-open. See "Coordinator work window" below. |
edit-guard.js |
PreToolUse (Write/Edit/MultiEdit/NotebookEdit) | Blocks a COORDINATOR from editing files directly — requires delegating the edit to a subagent (always allowed; DevSwarm-aware block wording when the liveness supervisor is active, topology-aware: "primary/main orchestrator" vs "sub-orchestrator"). Root-anchored allowlist (CLAUDE.md/AGENTS.md/GEMINI.md, .claude/**, .omc/**, .anti-hall/**, root PLAN.md/plan.md/STATE.json/CONTINUE-HERE.md, *.continue-here.md, the out-of-cwd .claude/projects/**/memory/** store), extensible via ANTIHALL_EDIT_GUARD_ALLOW. Skip-guard hatch: edit-guard (not in the destructive set). Fail-open. v0.64.0: also exempts PLAN MODE for non-source targets, narrowed by an isLikelySource classifier so undelegated source-file writes stay blocked even in plan mode. |
coordinator-detect.js |
Shared module (not a hook) | The single coordinator-vs-subagent discriminator, extracted from command-guard.js so edit-guard.js reuses the exact same detection logic instead of duplicating it. |
ask-guard.js |
PreToolUse (AskUserQuestion) | OPTIONAL, default off (guards.noBlockingQuestions). Enforces the "do not hold work on a question" rule: advise adds a reminder, block refuses the call unless the first question starts with DESTRUCTIVE: or CREDENTIAL:. Never infers what the user wants from the transcript; fail-open; Claude only (Codex has no ask tool). Also adds the agents-in-flight note (guards.questionAgentsNote, default on, independent of the mode). |
model-routing-guard.js |
PreToolUse (Agent/Task) | Anti-waste routing — classifies spawn descriptions (mechanical vs complex) and blocks/advises toward the cheapest fitting model. Strict by default (v0.35.0+): unconditional block on omitted-model mechanical spawns. Set ANTIHALL_MODEL_ROUTING=advisory (project-scoped env) to opt out and revert to advisory-only. Debate role-words in spawn description downgrade row-1 block to advisory. Fail-open; unknown model tokens always allowed. |
omc-detect.js |
Shared helper (not a hook) | Detects whether an oh-my-claudecode autonomous loop is active + fresh. Consumed by task-guard / tasklist-guard to suppress Stop-blocks to advisory when an OMC loop is running, preventing deadlock. Fail-open = NOT deferring. Kill-switches: DISABLE_OMC=1 or OMC_SKIP_HOOKS including persistent-mode. |
hooks/lib/devswarm-detect.js |
Shared helper (not a hook) | OPTIONAL, feature-gated — mirrors omc-detect.js for the opt-in DevSwarm liveness supervisor: reports whether it should be considered active for this session/environment. Dormant (zero effect, byte-for-byte identical to today) unless DEVSWARM_REPO_ID is set (auto mode) or ANTIHALL_DEVSWARM_SUPERVISOR=on. Consumed by doctor.js's per-workspace DevSwarm check. Fail-open = NOT active. Kill-switch: DISABLE_ANTIHALL_DEVSWARM=1. |
hooks/lib/devswarm-role.js |
Shared helper (not a hook) | OPTIONAL — topology gate distinct from devswarm-detect.js: answers only "is THIS session a DevSwarm CHILD workspace?" via DEVSWARM_SOURCE_BRANCH (non-empty = child, empty/unset = Primary). Fail-open = Primary. Consumed by devswarm-child-role.js. |
hooks/devswarm-child-role.js |
SessionStart | OPTIONAL, feature-gated — Layer 1 of the DevSwarm layered recovery model: for a DevSwarm CHILD workspace only (both devswarm-detect.js active AND devswarm-role.js child), injects a reminder to proactively self-report idleness (hivecontrol workspace message-parent) rather than sit unnoticed. Silent no-op for Primary/non-DevSwarm sessions. v0.65.0: also injects the blocking-question escalation protocol into every workspace — a child forwards a blocked decision to its parent (send --to-primary) with the options, its recommendation, and the default it will take if unanswered by its deadline, keeps working every other unblocked item, and proceeds on that default while flagging the assumption loudly; only an unauthorized destructive/irreversible action is a hard stop. Ladder is child → parent → human, never child → human; the parent side gets the matching reply-and-escalate directive. |
devswarm-parent-inbox.js |
UserPromptSubmit | OPTIONAL, feature-gated — mechanical trigger for the "Primary neglects child workspaces" failure (claude-code#39755). For a Primary DevSwarm session only: each turn surfaces the real unread/idle state of active workspaces, and recommends archiving any workspace the store derived as complete (archive_ready). Reads the durable-inbox files + the supervisor's verdicts + summary.json; never runs git/computeLiveness on the hot path. (0.54.1) Also injects a compact live table EVERY turn — one row per active workspace: status (escalated>stale>archive-ready>idle>active>dormant, attention first) / finish-rate (required gates met/total + optional heartbeat %) / unread / last-activity; capped at 12 rows with a logged +N more. (0.60.0) A row idle beyond ANTIHALL_DEVSWARM_IDLE_MS (default 6h, ms) is relabeled active→idle — a view-only demotion (no delete, no gate change, no archive) so a long-verified-done workspace stops reading as "active" forever; never overrides escalated/stale/archive-ready. (0.67.0) Each row now renders name (shortid) instead of a bare UUID, reading the devswarm-names.js fs cache only — never spawns hivecontrol on this hot path — falling back to the raw id when no name is cached. (v0.70.0) The normal-tier unread segment's wording was softened from a per-turn "STOP ... before continuing" imperative to advisory phrasing (tierOf already routes urgent/high workspaces to the separate loud segment, which is unchanged). (v0.70.1) New dormant tier (rank 5, sorts last, below even active): a DevSwarm mesh/registry row outlives its workspace — closing a workspace in the DevSwarm app deletes nothing (registry row, worktree, descriptor, and hivecontrol workspace list entry all survive) — so a row whose newest known activity signal is at least ANTIHALL_DEVSWARM_DORMANT_MS old (default 30 min, ms) is labeled dormant instead of active/idle, since only heartbeat/verdict AGE reliably separates a live workspace from one closed with no teardown signal. It demotes, it never hides — a dormant row still renders with its unread count, only ranked last, so a genuinely-live-but-quiet workspace can never go invisible; it never overrides escalated/stale/archive-ready, and the threshold is a heuristic, not proof a workspace is closed. (v0.74.0) Report-only git ground-truth risk markers are appended to a row's workspace-title cell (never a new column): local only (not pushed) or ⚠ N unpushed (from the child's heartbeat-carried gitPushState probe, companion/lib/devswarm-git-truth.js), and merged (unverified) on an archive-ready row whose merged gate was self-declared but never proven by git ancestry (merged_verified !== true). (0.111.0 wording): the no-upstream marker dropped its ⚠ warning glyph and reads local only (not pushed) — a child created by spawn with a branch that has never been pushed is the normal/expected state, not an error; the underlying noUpstream field and its priority over a bare unpushed count are unchanged. Never blocks or implies anything beyond "look before archiving". Silent no-op otherwise. (v0.100.0) Archived rows (label EXACTLY archived; a real backlog on an archived row escapes via not-draining, rank 1.5, so it is never hidden by this filter) are now dropped from the roster table BEFORE the sort/cap, not after — previously an archived row could compete for a table slot on equal footing with a live one and push it into overflow. Default ON via ANTIHALL_ROSTER_HIDE_ARCHIVED (0 restores the old behavior); the count of hidden archived rows is always named as a +N archived note, never silently dropped. The cap itself is now configurable via ANTIHALL_ROSTER_MAX_ROWS (default 12, was hardcoded). Separately, the advisory recent[] broadcast feed — previously rendered the FULL message body verbatim every turn, with no memory of what was already shown — now truncates each body to 200 chars with an ellipsis, drops a broadcast older than ANTIHALL_BROADCAST_MAX_AGE_MS (default 24h), and per-session-dedupes against ~/.anti-hall/devswarm/parent-inbox-broadcast-seen/<session>.json (bounded to 200 keys) so a broadcast no longer re-injects verbatim for the rest of the session. Fails open to showing (never hiding) on any dedup-state read/write failure. (v0.106.0) An archive-ready workspace from a PREVIOUS session whose entire unread backlog is the Primary's OWN archive-request send (archive_request_only_unread, companion/lib/devswarm-store.js) and whose session is no longer running is excluded from the urgent/attention nag — it never clears on its own (nobody is left to drain it); the row still gets the existing cooldown'd archive-ready nudge and still shows in the table, just as archive-ready rather than not-draining forever. A new user-editable ignore list, ~/.anti-hall/devswarm/ignore.json ({"ids": ["<id>", ...]}, companion/lib/devswarm-ignore.js), additionally suppresses the same nag for any explicitly-listed id while leaving it fully visible in the roster table — it only ever changes whether a turn nags, never what is tracked. While child questions are unanswered, the Primary's own-inbox notice leads with QUESTIONS AWAITING YOUR REPLY: N (oldest Xm) — <workspace title>: <first 80 chars> (preview scrubbed of secrets and control characters; omitted when the stored summary has no question text). |
devswarm-parent-gate.js |
Stop | OPTIONAL, feature-gated — Primary-only, capped/loop-safe (Phase 5 shared Stop policy hooks/lib/stop-policy.js: honors stop_hook_active; cap keyed by stable block kind, not unread counts). Blocks the Primary from ending its turn while a child still has unread backlog past its cursor OR the supervisor already judged a child stale/escalated OR (0.56.0) the Primary's OWN summary-projected unread is nonzero (read from summary.json, no DB open) — surfaced with the same imperative "STOP and read them FIRST via devswarm.js inbox read-primary <id>" wording (Phase 5: then run the returned ackCommand, inbox ack-primary <id> --receipt <rid>) the child gate uses, so a Primary can no longer end its turn while sitting on its own unread inbound. (0.61.1) "Unread backlog" now counts only REAL unread — system-generated poke/mirror noise (companion/lib/devswarm-noise.js isNoiseText) is excluded, closing a ghost-workspace feedback loop where a backlog consisting solely of the Primary's own mirrored poke nagged on every Stop; an unparseable row/unreadable inbox still counts as real (fail-open). Registration also now precreates an empty durable inbox so a freshly-registered child reads as known/empty, not absent. (0.62.0) A stale/escalated verdict is now suppressed (never gates on the liveness axis) when the workspace has a FRESH heartbeat — definitive proof-of-life, since a heartbeat is emitted only by the workspace's own live session — while the real-unread coordination axis is untouched (a live, heartbeating workspace with genuine unread backlog still gates). Reads only files (fs cursor + the supervisor's verdict file + the summary projection + the heartbeat file) — no git, no live liveness on the ~30 s Stop path. Fail-open. (v0.69.0) The gate can no longer be satisfied by merely READING a child's blocking --question — it now requires an OBSERVED reply, checked against a durable per-project reply-state file (companion/lib/devswarm-reply-state.js) that devswarm-parent-reply-tracker.js writes on every successful send --to <id> --question. The forced-ack cap can no longer silence an unanswered question forever — once exhausted it escalates once with distinct wording instead of going silently quiet; every Primary turn also re-asserts the obligation. (v0.71.0) devswarm-reply-state.js's storage is now an append-only JSONL log instead of a lockfile read-modify-write: recordReply is one O_APPEND write (no lock), readReplyState folds the log with a fail-closed newline separator, a 480-byte record cap, and a __proto__-safe fold accumulator — structurally eliminating the disclosed steal-branch TOCTOU. A loss-safe migration (migrateReplyState) is wired into update.js/doctor-repair.js. (v0.80.0) A stated intent (the gate invites the Primary to say a block is intentional) is now recorded against the exact condition signature it was stated about and suppresses ESCALATION ONLY while that signature is unchanged — the first block on any condition still always fires, the reason text is stored but never echoed back (closed-vocabulary boolean only, since reasons reaching the model as text are an injection surface), and any change to the signature (unread arrives, a workspace's status changes) drops the suppression and resumes normal escalation. The absolute escalation cap remains as a backstop. Ships with a forward migration for the new persisted intents map, wired into both the updater and the doctor, fail-open on a pre-upgrade state file with no intents key. (v0.84.0) The gate no longer hides genuinely drainable mail: a permanently-stale persisted repoKey and an ownerKey-only descriptor both used to make real unread invisible here. Partition resolution now goes through the SAME shared helper the CLI uses (companion/lib/devswarm-repokey.js registeredRepoKey), so the gate and the inbox read-primary command it prescribes can no longer disagree about which workspaces a session owns. v0.85.0 dead descriptor vs. neglect: the old rule — known:false on the unread read ALWAYS blocks, unconditionally, INCLUDING an absent inbox file — was itself the defect: a descriptor that outlived its sibling's archive can never produce an inbox, so the gate blocked every Primary turn permanently with nothing available to clear it. An inbox-missing (ENOENT only) on a descriptor whose worktreePath is ALSO provably gone no longer raises the unknown axis. Nothing is hidden — store-side unread still blocks on unionUnread, a stale/escalated verdict still blocks, and every other unreadable reason (inbox-unreadable, cursor-*, no-inbox-path, read-threw) still blocks regardless of the worktree. Fail-closed-to-block: "gone" is a definitive ENOENT lstat on an ABSOLUTE path only; a relative/missing path, a dangling symlink, or any other stat failure is NOT provably gone. v0.93.0: app-side archived-but-still-live senders keep blocking — a question is informational only when its sender matches no registry row of any kind (active OR archived) and no descriptor; the gate folds archivedRegistryRows into its known-registry set and fails open to blocking on a legacy summary that lacks the field. Sender attribution (shared with devswarm-parent-inbox.js) now excludes the recipient's own identity family first, so a reply from the recipient or a twin never wrongly clears a question from the true sender. v0.94.0: when a worktree's meshId maps to more than one sibling registry row, sender attribution is now picked by companion/lib/devswarm-attribution.js's pickAttributionRow — a pure function of row values (real-sessionId row wins, then the branch-slug row, then ascending lexical id) — replacing the old liveness-based picker whose present-tense signals could flip the reported sender for the same stored message across passes (defect f3b8f326bfc3). v0.106.0: the same ~/.anti-hall/devswarm/ignore.json ignore list devswarm-parent-inbox.js now honors (see that row's v0.106.0 note) also drops a listed id from this gate's neglect computation entirely — never a reason a Primary's turn blocks — while leaving it fully visible everywhere else. New (Unreleased): the plain NEGLECT nag (blocking-workspace backlog only — never the unanswered-question/truncation/escalation paths, which bypass the cap unconditionally by design) downgrades to advisory when a newer build is already registered (installed_plugins.json) but not yet loaded this session — see hooks/lib/stop-version-gate.js. The pre-existing per-signature intents/intentAcks forced-ack (devswarm.js gate-intent --reason) is unchanged; the new shared hooks/lib/stop-ack.js ack mechanism was deliberately NOT wired into this gate, to avoid destabilizing its escalation-ceiling logic — it covers silent-agent-nudge.js/tasklist-guard.js, which had no user-triggered ack at all. |
devswarm-parent-reply-tracker.js |
PostToolUse (Bash) | NEW in v0.69.0, OPTIONAL/feature-gated, Primary only, observe-only — watches every Bash call for a successful devswarm.js send --to <id> --question ... and records it via devswarm-reply-state.js's recordReply(), keyed by a durable per-project repoKey (not a Claude session_id) so devswarm-parent-gate.js can tell "read" apart from "decided and replied". Anti-spoof guarded (the response shape alone is not trusted without the input command also plausibly being a real send call); never blocks, writes nothing to stdout; fail-open on every error. |
devswarm-child-turn.js |
UserPromptSubmit | OPTIONAL, feature-gated — child-only. Writes a turn-authored heartbeat (heartbeats/<DEVSWARM_BUILDER_ID>.json, unique per child — falls back to a sanitized/hashed <branch> key only when DEVSWARM_BUILDER_ID is absent; never a background ticker) and reminds the child to report to its parent. (0.54.1) Also surfaces a non-destructive unread-count check against the child's OWN durable descriptor inbox — PARTIAL: this only makes an already-populated durable inbox visible to the child; nothing shipped yet drains the child's native parent→child queue into it (v0.54.2 follow-up). (0.56.0) The unread surfacing is now IMPERATIVE PRIORITY wording ("STOP and address... FIRST"), scans unread lines for a [[ANTIHALL_ARCHIVE_REQUEST]] marker and surfaces a distinct confirm-then-archive segment when found, and mechanically writes/refreshes the child's own descriptor every turn (MERGE-preserving) so the parent can always discover it. Silent no-op otherwise. (v0.69.0) Self-continue directive: tells the child to keep issuing tool calls across rounds of a multi-round autonomous task within the same turn, reserving Stop for a genuine block, final completion, or an unrecoverable error — cutting the wake-cycle (supervisor cron) latency a per-round idle-out previously cost. Shared verbatim with the Codex port. (v0.74.0) writeHeartbeat also attaches one report-only gitPushState probe per turn (companion/lib/devswarm-git-truth.js) — noUpstream/unpushed, resolved from the worktree at cwd, omitted entirely (never fabricated) when the worktree or probe doesn't resolve — surfaced to the parent as a risk marker by devswarm-parent-inbox.js. v0.85.0: now participates in the SAME per-id advisory lock the archive/retire paths use (companion/lib/recovery.js's acquireLock, bounded ~1s sync retry, fail-open to an unlocked write) around its own descriptor rename — an atomic replace installs a NEW INODE at workspaces/<id>.json, and an unlocked interleave with a verify-then-unlink-by-pathname retirement could silently de-register a LIVE child. One lock, over mkdir+write+rename, no nested acquisition, no subprocess. |
devswarm-child-gate.js |
Stop | OPTIONAL, feature-gated — child-only, capped/loop-safe. Forces the child to self-report to its parent before going idle, so a child that finishes a turn pings the parent instead of dropping off its radar. (0.54.1) Heartbeat-freshness silencing REVERTED: the brief v0.54.0 "fresh heartbeat satisfies the gate" logic false-silenced a child that worked <5 min then stopped without reporting, so the gate always demands at least one real report per unchanged blocking state, bounded by the per-window cap MAX_BLOCKS = 2 plus a never-resetting MAX_BLOCKS_PER_SESSION = 6 lifetime cap (v0.98.1, defect a55d6b71a76f). A benignly-dropped heartbeat --summary still counts as an attempted report via a session/nonce-authenticated local attempt record — appended per writer id under summary-attempts/<repoKey>/<writerId>.ndjson (the reader also still honors a legacy flat summary-attempts/<repoKey>.ndjson file if present) — not a re-prescribed loop. (0.56.0) STRICT mode (ANTIHALL_DEVSWARM_CHILD_GATE_STRICT, default ON) backs the durable-inbox check with one bounded non-destructive hivecontrol workspace message-count probe (5 s timeout) when the durable check shows nothing, to catch a native backlog the child never inbox pulled. Fail-open. (v0.73.0) Reads the shared union unread primitive (companion/lib/devswarm-unread.js, NDJSON ∪ store-only mesh-direct backlog, hash-deduped) instead of NDJSON alone, and splits its messaging into "CHILD NOT DRAINING" vs "YOUR INBOX" segments naming the workspace by title. |
devswarm-child-drain.js |
PostToolUse (Bash) | NEW in v0.73.0, OPTIONAL/feature-gated, child-only, throttled. Closes the downstream project field gap where a DevSwarm child has no mid-turn re-entry: devswarm-child-turn.js fires once per UserPromptSubmit, never during a long autonomous task, so a mesh-direct send --to (store-only, invisible to an NDJSON-only reader) could sit unnoticed while the child kept working. Mirrors the Primary-only devswarm-parent-reply-tracker.js (same PostToolUse/Bash registration shape) but child-only: on every Bash call, reads the shared union unread primitive against the child's own descriptor and, when unread > 0, injects a drain reminder — throttled to re-inject only when the unread count changes or a 10-minute window elapses, so a busy child isn't nagged on every tool call. Fail-open throughout. |
monitors/monitors.json + companion/lib/devswarm-wake-watch.js |
Harness monitor (not a hook) | OPTIONAL/feature-gated. monitors/monitors.json declares one plugin monitor, devswarm-wake-watch, with "when": "always": the harness starts node "${CLAUDE_PLUGIN_ROOT}/companion/lib/devswarm-wake-watch.js" --auto at every session start. The watcher is pure-read and poll-based (devswarm.wakeWatchPollMs): it watches this workspace's own direct mesh mail and emits one stdout line the moment new mail lands, waking an idle session faster than the cron fallback. --auto marks a harness start: because every stdout line is a transcript event, the expected refusals (not-a-devswarm-session, disabled-by-settings) stay silent on stdout, so a plain terminal session never burns a turn on them (0.119.0); a model-armed watcher without --auto still prints its one refusal line. Additive only: never replaces the CronCreate fallback; off via devswarm.wakeWatch. v1 watches directs only, not broadcasts. |
swarm-guard.js |
PreToolUse (Agent/Task) | Anti-fork-bomb — spawn-rate cap + real reclaimable-memory check (vm_stat / MemAvailable, not os.freemem()). A blocked spawn also logs one line to ~/.anti-hall/swarm-trips.log (observation only — doesn't feed the rate window). |
phase-tracker.js |
PreToolUse (Agent/Task) | Records every subagent spawn so the statusline shows live swarm activity. It also writes a rolling ~/.anti-hall/agents/recent-spawn.json heartbeat that agentsRunning() consumes, so the Stop guards know when parallel work is live. Never blocks. |
agent-watchdog.js |
CLI helper (not a hook) | Heartbeat enforcer — scans ~/.anti-hall/agents/*.json and reports stale/hung subagents; run manually by the orchestration skill. |
task-tracker.js |
UserPromptSubmit | Injects task-list discipline (capture, prioritize, work in order) + a one-line freshness note when open/stale tasks exist. v0.76.0: state writes now go through hooks/lib/state-prune.js (see below). |
hooks/lib/stop-ack.js |
Shared module (not a hook) | New (Unreleased). The one shared signature-ack mechanism for advisory nudge-class Stop hooks (peer complaint: "let me ack a condition I've already confirmed false for the session"). signatureFor(subject) hashes a caller-supplied, content-derived string; isAcked/recordAck read/write a per-session file ~/.anti-hall/stop-ack/<session>.json mapping "<hook>:<signature>" -> ack timestamp — a documented skip-file entry the agent writes once the user has explicitly confirmed the condition is a false positive (same convention as skip-guard.js's ~/.anti-hall/skip.json, but per-signature/per-session rather than per-guard/TTL'd). Wired into silent-agent-nudge.js and tasklist-guard.js. Kill switch guards.stopAck (default on) / ANTIHALL_STOP_ACK=off. Fail-open: any error -> not acked -> blocks normally. |
hooks/lib/stop-version-gate.js |
Shared module (not a hook) | New (Unreleased). isStale(pluginRoot, {env, home}) detects when installed_plugins.json (harness-owned) has already registered a newer anti-hall version than the one this hook process is running — reuses skills/update/scripts/update.js's own resolvePaths/versionFromInstalledJson/isSemver/compareVersions exports (same comparison doctor.js already performs read-only), never reimplements the parsing. Wired into silent-agent-nudge.js, tasklist-guard.js, and devswarm-parent-gate.js's plain NEGLECT nag only. Prospective only — a hook build that predates this file cannot self-check. Kill switch guards.stopHookVersionDowngrade (default on) / ANTIHALL_STOP_HOOK_VERSION_DOWNGRADE=off. Fail-open: any error -> not stale -> blocks normally. |
hooks/lib/state-prune.js |
Shared module (not a hook) | New in v0.76.0. Bounds per-session state files under ~/.anti-hall so they can't grow without limit. Every per-session state file (task-tracker, speculation-guard, tasklist-guard, codex-nudge) was kept forever and never read back once its session ended — on a heavy multi-session machine this reached 47,000+ files across 71 days, worsened by doctor.js's own self-tests orphaning one file per hook per run. pruneStale() is wired into each of those four hooks' existing write paths (no new hook, no new event): removes same-prefix files past a 7-day TTL, throttled to once per 6 hours via a stamp file, never removes the current session's own file, fails open on any fs error. |
limit-conserve-inject.js |
UserPromptSubmit | Limit-conservation mode. Injects a token-conservation nudge when context usage reaches ANTIHALL_LIMIT_THRESHOLD (default 85%). ANTIHALL_LIMIT_CONSERVE: auto (default) reads the OMC usage cache; on forces the nudge; off disables. Auto mode requires OMC; without it, manual on/off only. Skip-guard hatch: limit-conserve. |
limit-conserve.js |
Shared helper (not a hook) | Reads the OMC usage cache and applies threshold logic; consumed by limit-conserve-inject.js. Account-aware: tracks the logged-in Claude account's userID (~/.claude.json) alongside the usage cache's mtime; if the account changed since last seen and the cache hasn't been refreshed under the new account yet, the stale reading is deactivated rather than mis-applied across accounts. Kill-switch: ANTIHALL_LIMIT_ACCOUNT_CHECK=off. |
auto-handover.js |
UserPromptSubmit | New in v0.108.0, ON by default. When the main agent's context first crosses autoHandover.pct (default 85% of this session's ACTUAL context window — see hooks/lib/context-pct.js) or the opt-in absolute autoHandover.maxTokens ceiling (default 0 = off; env ANTIHALL_AUTO_HANDOVER_MAX_TOKENS), whichever comes first, tells it — without asking the user first — to self-write an anti-hall handover (never delegated), tell the user and list the saved paths, and urge /compact//clear with an exact /compact focus: <handover path> line to paste. Context % comes from hooks/lib/context-pct.js: the statusline's real context_window figure (persisted by statusline/phase-bar.js), else a Codex rollout's model_context_window, else a transcript estimate (window from ANTIHALL_CONTEXT_WINDOW_TOKENS, the session's last-seen statusline window, or "inferred 1M" once usage passes 200k). With a genuinely unknown window it sends one soft advisory instead of the mandatory directive. Fires once per arm; milestone reminders every nagStepPct (5) further points; re-arms when usage drops back below. ANTIHALL_AUTO_HANDOVER_PCT overrides the threshold (0 = off). Shared with the Codex port. v0.109.0 post-handover new-work gate (autoHandover.gateNewWork, default on): once this session's handover file exists, each prompt carries a short directive — size the request BEFORE starting; above autoHandover.gateBudgetPct (default 5) points of the window, offer to park it in the task list + handover until after /compact or /clear, or proceed if the user insists (quick questions, the in-flight task and DevSwarm spawns pass through) — plus ONE measured backstop per handover once usage grows more than that budget past where the handover was saved. |
auto-handover-pause-nag.js |
Stop | The auto-handover trigger's companion. If the fire directive has not gone out this arm and the agent is over threshold at a Stop (a long autonomous turn that never reaches a new prompt), it delivers the directive once (shared latch, never while stop_hook_active). Otherwise, once it has fired this session and usage is still over threshold, sends one short reminder at a genuinely quiet point — no open TodoWrite work, no subagent spawned in the last 2 minutes — at most once per nagQuietMin minutes (default 15). Uses the shared stop-policy.js stop_hook_active check so its own block-with-reason (the only non-blocking-adjacent way a Stop hook can surface text in this harness) is never re-triggered by its own answer. Silent when autoHandover.nag is false or the feature is disabled. v0.109.5 decisive prompt (autoHandover.decisivePrompt, default on): once this session's HANDOVER*.md exists and is fresh (hooks/lib/handover-freshness.js mirrors tasklist-guard.js's own staleness rail — a counted file-changing action timestamped after the handover's mtime), appends an instruction to END the reply with one glyph-led line: 🟢 **GOOD POINT TO /compact NOW** (or /clear//new when the handover's own Open items/Next action read as done), or ⚠️ **Refresh the handover first**, then /compact when stale — never both. Shared verbatim with the Codex port (/new in place of /clear). |
precompact-snapshot.js |
PreCompact (manual + auto) | New in v0.108.0. Safety net for the self-written handover: right before every compaction writes a MECHANICAL .anti-hall/handovers/<date>/<session>/PRECOMPACT-<n>.md — pwd, git branch/HEAD/dirty files, the task list parsed from the transcript, the last 10 user messages verbatim, and a pointer to the newest HANDOVER*.md. Always exits 0 and prints nothing, so it can never block compaction. handover-resume.js names it on the next SessionStart. Shared with the Codex port (Codex PreCompact). |
repair-on-reload.js |
SessionStart + UserPromptSubmit | New in v0.108.0. Repairs run on a plain /reload-plugins or a new session on a new version, not only via update.js / doctor --repair. When any default migration in companion/lib/migrations.js is not stamped at the running version or newer (one small read of ~/.anti-hall/update-sweep-state.json), it takes ~/.anti-hall/repair-on-reload.lock and spawns one detached doctor.js --repair --migrations-only --quiet (the newest cached version's doctor, never one older than the running version). It runs only the stamped data migrations and store repairs; statusline, Codex hook install, supervisor and anything else that writes user config stay behind a user-typed doctor --repair. One migration touches the project in the session's cwd: legacy state files are copied (never moved) into .anti-hall/history/legacy/. Since 0.108.5 the GSD .planning/ fold never runs here; it is the explicit, copy-only migrate-state.js --planning, and doctor reports worktrees where the pre-0.108.5 fold moved tracked .planning/ files, with the restore command. It logs to ~/.anti-hall/logs/repair-on-reload-*.log (newest 5 kept). At most one run per hour per running version, counted only from a spawn that started (~/.anti-hall/repair-on-reload.last.json); the child runs at nice 19. Nothing pending → silent no-op. Subagent payloads skipped. Off: ANTIHALL_REPAIR_ON_RELOAD=off. Shared with the Codex port. |
hooks/lib/settings.js (+ settings-schema.js) |
Shared module (not a hook) | New in v0.108.0. The one settings store, ~/.anti-hall/settings.json: a declarative schema (sections, types, bounds, env names, /config option names, legacy sources) and get/getWithEnv/set/reset/source. Precedence env > settings.json > /config > legacy file > default (legacy outranks /config until the one-time migration is stamped). Dotted keys are read flat or nested. Used by every guard, Jev, auto-handover, the version alerts, the statusline, and DevSwarm (incl. auto-archive and retention). Front ends: /anti-hall:settings, scripts/settings.js. See Settings. |
task-guard.js |
Stop | Blocks once if the session ends with open tasks. |
tasklist-guard.js |
Stop | Blocks when non-trivial work (≥ threshold file-mutating actions) wasn't tracked as tasks or lacks a fresh per-session progress file (.anti-hall/progress/<date>/<session-id>.md); coexists with task-guard with its own independent block cap; capped + fail-open. v0.76.0: state writes now go through hooks/lib/state-prune.js (see above task-tracker.js entry). Already short-circuits on hash === lastHash before blocking again (never re-blocks an identical signal). New (Unreleased): honors a per-signature session ack and downgrades to advisory when a newer build is already registered but not yet loaded — see hooks/lib/stop-ack.js / hooks/lib/stop-version-gate.js below. |
skip-guard.js |
Escape hatch (shared primitive) | TTL'd ~/.anti-hall/skip.json user-override read by the guards; granular per-guard, and a broad all skip excludes the destructive git-guard (must be named explicitly). |
version-alert.js |
SessionStart (non-blocking) | Tells the agent to inform the user when anti-hall is behind. Two cases: the plugin-cache mirror (~/.claude/plugins/cache/anti-hall/anti-hall/<v>/) already holds a newer version than the running one → "reload" only (with that version's changelog headline); the remote is newer (cache ~/.anti-hall/version-check.json, 2 h TTL — was 24 h, which missed multi-release days) → "update, then reload". Once per session per case. A stale/absent cache spawns a detached git ls-remote --tags refresh and stays silent — never blocks on network. installed_plugins.json is never trusted (it can lag). Off: versionAlerts.antiHall=false / ANTIHALL_VERSION_ALERT=off; skip-guard hatch. |
devswarm-version.js (+ devswarm-version-refresh.js) |
SessionStart (non-blocking) | New in v0.76.0, OPTIONAL/feature-gated. Probes the installed DevSwarm version and flags drift from the baseline anti-hall was verified against — command-guard.js matches DevSwarm subcommands by literal string, so a renamed verb in a future DevSwarm release would make a block silently stop matching with nothing to signal it. Mirrors version-alert.js's shape: a fresh cache short-circuits, a stale/absent one spawns a detached, unref'd background probe (devswarm-version-refresh.js) and returns immediately so session start is never blocked. Drift classification is semver-aware — major/minor advises, patch-only stays silent, a downgrade is worded accordingly; the advisory dedupes on (installed, baseline) so it never nags twice for the same drift. Absent DevSwarm or unparseable output fails open and silent. Baseline lives in the shared hooks/lib/devswarm-baseline.js module, also consumed by the doctor check. Registered once, shared by both the Claude plugin and the Codex port. |
claude-cli-version.js (+ claude-cli-version-refresh.js) |
SessionStart (non-blocking) | New in v0.79.0. Probe 2 of anti-hall's drift-probe family. Detects the installed Claude Code CLI version and flags major/minor drift from the version anti-hall's harness-feature KB (docs/KB-claude-code-harness-features.md) was last audited against. Mirrors devswarm-version.js's shape: a fresh cache short-circuits, a stale/absent one spawns a detached, unref'd background probe (claude-cli-version-refresh.js) so session start is never blocked. Patch-only drift stays silent; deduped on the (installed, baseline) pair. CLI absent or unparseable fails open and silent. |
repo-self-drift.js |
SessionStart (non-blocking) | New in v0.79.0. Probe 3 of anti-hall's drift-probe family — deterministic, no network. Two checks: (1) parses docs/KB.md's own claimed hook/skill counts and compares against the actual count on disk, advising on either mismatch; (2) tracks the date the model KBs (docs/opus-4-8-features.md etc.) were last audited and advises past a 60-day threshold, since model facts aren't locally discoverable and a probe that can't verify would either invent an answer or fail constantly. Cached (<24h), deduped, fail-open and silent on any error. |
fable-availability.js |
SessionStart (non-blocking) | Reads ~/.claude.json's modelAccessCache/additionalModelOptionsCache (the same cache Claude Code's own /model selector renders from) once per session — no live API probe, fail-open, silent unless Fable is actually available. When available, threads args.fableAvailable=true into ship-it/deadly-loop Workflow invocations so the Reviewer seat's fallback chain extends to Fable → Sonnet → Opus. |
codex-availability.js |
SessionStart (non-blocking) | OS-agnostic PATH probe (Windows PATHEXT-aware) for a real codex executable; writes ~/.anti-hall/codex-availability.json ({available, checkedAt, source}) once per session so coordinators/skills read the cached fact instead of re-probing. Proves reachability only, NOT authentication/readiness — a runtime spawn can still fail even when available:true. Registered on both the Claude plugin and the Codex port. Fail-open. |
handover-resume.js |
SessionStart | On a fresh session (including after /clear or compaction), surfaces the latest .anti-hall/handovers/ entry (if any) and guides a structured resume from it — supersedes the lossy default compact summary. Fail-open (silent no-op if no handover exists). Registered on both the Claude plugin and the Codex port. |
defect-nudge.js |
SessionStart | New in 0.78.0. Once-per-day, non-blocking notice of open defects filed against anti-hall via the defect channel (below) — counts and ages only, never reporter-supplied text. Registered on both the Claude plugin and the Codex port. |
jev-review-reminder.js |
SessionStart | Durable "time to review the Jev shadow numbers" nudge. Most Jev integrations default to shadow (consulted + logged, never trusted) until an owner reviews jev report and promotes/demotes them; a per-session reminder would die with the session, so the check and its state (~/.anti-hall/jev-review-state.json) live in the plugin. Silent when Jev is off, jev.reviewReminder is false (default true), the payload is not the main session, or nothing is due. Also carries the "Recommended: enable Jev" notice (hooks/lib/jev-recommend.js): shown only while Jev is OFF and jev.recommendNotice is not false, once on first install then at most every 30 days (stamp ~/.anti-hall/state/jev-recommend-notice.json), via the same Tell the user now additionalContext channel as version-alert.js; doctor prints the same recommendation while Jev is off. Fail-open, exit 0. |
jev-weekly-scorecard.js |
SessionStart | Once-a-week, advisory nudge pointing at /anti-hall:jev when the 7-day jev report shows an integration has earned a KEEP or REMOVE verdict but its jev.json mode has not been promoted/demoted to match. Never changes a mode itself (read-only). Silent when Jev is off, weeklyNotice is false (default true), the session is a DevSwarm child, or the weekly latch (~/.anti-hall/state/jev-weekly-notice.json) shows a check within 7 days. |
emit-dedupe-reset.js |
SessionStart | New in 0.103.0. Writes a per-session reset marker on every SessionStart source (startup/resume//clear/compaction) so hooks/lib/emit-dedupe.js's UserPromptSubmit suppression re-emits blocks the fresh context lost, instead of treating them as already-seen. State-only, no context injected, fail-open. Registered on both the Claude plugin and the Codex port. |
task-lifecycle-log.js |
TaskCreated + TaskCompleted | Log-only: appends one line per task-lifecycle event to .anti-hall/history/<date>/<session-id>.md and maintains .anti-hall/history/INDEX.md, reusing session-history-index.js's idempotent append helper (the same one tasklist-guard.js calls). No matcher, no evidence gate, never blocks, no context injected, fail-open. Claude-only — Codex's hook runtime does not expose these events (see codex/README.md). |
speculation-guard.js |
Stop | Blocks once when the reply being stopped (payload last_assistant_message, transcript tail as fallback) contains hedge-word speculation without an evidence/uncertainty acknowledgment. Always-on (lexical, Tier 2). |
speculation-judge.js |
Stop | OPT-IN semantic judge (judges the reply being stopped: payload last_assistant_message, transcript tail as fallback): calls an LLM to catch confident inference-as-fact with no hedge word. Off by default; enabled by the jev.semanticJudge setting or ANTIHALL_SEMANTIC_JUDGE=1. |
claim-ledger.js |
Stop | new in 0.100.0. LEDGER-ONLY deterministic cross-check: records checkable tokens in the reply being stopped (payload last_assistant_message; when the transcript lags behind it, the transcript's last message counts as evidence; transcript tail as fallback) (counts, SHAs, task N of, N days ago, no-tool "still running") that never appeared in the session's tool output / hook context, to ~/.anti-hall/claim-ledger/<session>.jsonl. Never blocks; measures the false-positive rate before any blocking tier is enabled. |
codex-nudge.js |
Stop (advisory) | Nudges once/session for an independent Codex second-opinion review when substantial code shipped with no Codex review; off-switch ANTIHALL_CODEX_NUDGE=off. |
silent-agent-nudge.js |
Stop (advisory) | New in 0.109.0. Orchestration rule I already tells the coordinator to poll for a stuck background subagent and TaskStop+re-dispatch it after ~20 min of silence — nothing mechanically reminded it to. PRIMARY signal (the harness always produces it): scans a bounded tail of the main transcript (hooks/lib/transcript-tail.js, capped 1.5MB) for background-Agent launches ("Async agent launched successfully" tool_results carrying agentId:/output_file:) with no LATER terminal <task-notification> (<status>completed\|failed\|stopped</status>) — "silent" = that agent's output_file mtime (or its launch time, if the file is missing — missing counts as dead too) is older than guards.silentAgentNudgeMin (default 20 min). SECONDARY signal (additive, kept): the ~/.anti-hall/agents/<id>.json self-reported heartbeat convention. Nudges once, naming the agent's launch description (or id) and how long it's been silent. Never kills/stops anything itself — advisory text only, capped one nudge per stale snapshot (output_file mtime, or heartbeat ts) — a later change or a new staleness period can nudge again. Off-switch ANTIHALL_SILENT_AGENT_NUDGE=off; skip-guard hatch silent-agent-nudge. New (Unreleased): honors a per-signature session ack (hooks/lib/stop-ack.js, see below) and downgrades to advisory when a newer build is already registered but not yet loaded (hooks/lib/stop-version-gate.js, see below). Shared verbatim with the Codex port. |
stale-agent-stop-note.js |
PreToolUse TaskStop (advisory) |
Adds one line when the agent being stopped was sent a message (named teammate inbox) or resumed (background agent) after its last report and has not reported since: it may be working. A teammate's end-of-turn report is written to the coordinator's transcript only when the coordinator's turn yields, so the first report after a message can predate it. Never blocks. guards.staleAgentStopNote. The same state makes such a teammate count as running (pendingMessage) for task-guard / dispatch-demand until 20 min pass with no sign of life. |
idle-agent-sweep.js |
UserPromptSubmit (advisory) | Once per user prompt, lists agents that finished but were never stopped, with the exact call to end them. Claude: a named teammate whose newest event is an idle_notification with idleReason available or failed, with no later SendMessage to it and no TaskStop (an idle with no idleReason means it is waiting on its own work and is not listed; background agents are not listed, their completed notification already ended them). Codex: a multi_agent_v1 agent whose wait_agent result is completed/errored and that was never closed (close_agent) or re-tasked (send_input/resume_agent); an open agent holds a thread slot. The newer Codex collaboration tool set has no close tool, so nothing is listed there. Fires when guards.idleAgentSweepCount (3) are idle or one has been idle guards.idleAgentSweepMin (15) minutes; up to 10 names, oldest first. A <task-notification> turn is skipped; a queued burst gets one copy. Never blocks, never stops anything. |
merge-side-pick.js |
PostToolUse + PreToolUse (Bash), advisory | Records a wholesale side-pick conflict resolution and test runs per session; on a push with a side-pick not followed by a test run, adds one advisory (guards.mergeSidePickAdvisory, default on). Claude and Codex. Never blocks. On Codex the advisory is shown only by Codex builds that support PreToolUse additionalContext (rust-v0.129.0 and later, docs/KB-claude-codex.md section 5.2); older builds ignore it and the recorder stays harmless. |
compact-advice-guard.js |
Stop (blocks once) | New in 0.116.0. Field defect: the model declared "✅ SAFE TO COMPACT NOW" and repeated a /compact focus: … line a few turns after a manual /compact, at low context, on the sole basis that no background agents were running. Blocks once per declaration when the turn's final message recommends compacting (SAFE TO COMPACT, good point to /compact, /compact offered as an instruction — quoted, negated and retracted text excluded) AND context % (statusline → Codex rollout → transcript estimate, hooks/lib/context-pct.js) is below autoHandover.pct − guards.compactAdviceMarginPct, OR a compact boundary (Claude {"type":"system","subtype":"compact_boundary"}; Codex compacted / context_compacted) is within guards.compactAdviceRecentTurns turns. Unknown context % → only the recent-compact rule. Allowed: the threshold-fired auto-handover path (latch fired, no compact since, context not low). Honors stop_hook_active. Shared with the Codex port. 0.117.0: the shared matcher (hooks/lib/compact-advice.js) no longer fires on a single-quoted or backtick-quoted MENTION of the phrase, a question sentence, or a negated/conditional sentence (far from, not yet, once … it will be … ; first …); the bare safe to compact/clear wording now also requires a line/sentence start or the unambiguous ALL-CAPS form. |
compact-declaration-guard.js |
PreToolUse (Agent/Task/Write/Edit/MultiEdit/NotebookEdit/Bash) | New in 0.116.0 (opt-in, default OFF); re-enabled by default in 0.117.0. Once the current turn (since the last real user message; <task-notification>s do not reset it) holds a SAFE TO COMPACT declaration, blocks Agent/Task spawns, file edits and state-changing Bash (hooks/lib/work-detect.js + git push/git tag/gh pr merge). Read-only tools pass. Cleared by the next user message or an explicit RETRACT SAFE TO COMPACT line. Codex: registered for Bash only (the port registers PreToolUse for shell guards only). Shares the tightened matcher above (0.117.0). Toggle via guards.compactDeclarationGuard. |
ship-it-guard.js |
PreToolUse (Write/Edit/MultiEdit, and Bash) | OPT-IN, default OFF — the only opt-in code-edit gate. With ANTIHALL_SHIPIT_GATE ∈ {1,true,yes,on}, blocks a CODE edit on a hard-risk path (migration / auth / .github/workflows / security) when no PLAN.md exists (repo root). Also does a conformance advisory (never blocks) for edits outside a PLAN.md's declared files: list. Enforces artifact existence only (not plan quality), conservative, fail-open. No effect when unset. On Bash (guards.shellWriteChecks) the existence gate also covers shell-write targets (>/>>, heredocs, tee, sed -i, perl -i, cp/mv, python -c open-for-write), except writes into the session scratchpad or a tmp root outside a repo. |
merge-gate.js |
PreToolUse (Bash) | OPT-IN, default OFF — a backstop, not a guarantee. With ANTIHALL_MERGE_GATE ∈ {1,true,yes,on}, blocks an auto-merge (gh pr merge incl. --auto, gh pr review --approve, git merge --no-ff/--ff into main/master/develop, and hivecontrol workspace merge-into-source/merge-from-source) when the agent's own recent output carries an UNRESOLVED self-hedge ("pending review" / "first-pass" / "needs your eyes" / …) not signed off by the user (only a real typed user prompt after the hedge containing "owner approved"/"owner signed off"/"sign-off received"/"fidelity verified"/"verified against"/"resolved:" clears it — the assistant can never clear its own hedge, and peer/cross-session/hook-injected records do not count; hedges inside quotes/code are ignored). Keyword-heuristic, bypassable, fail-open, cannot hard-loop; no effect when unset. |
root-cause / orchestration / ship-it / deadly-loop (+ deadly-loop-multi, install-statusline, doctor, system-briefing, update, activate, simplify, debt, devswarm, handover, defects) |
Skills | Slash commands (see Skills). |
statusline/ |
Statusline | Rich line 1 for ANY repo (monorepo or simple); the monorepo/simple renderer is only a fallback if the rich renderer yields nothing. Line 2 is an always-on phase/context bar. |
companion/mcp-reaper.js (+ install-reaper.js) |
Interval companion (not a hook) | OPT-IN, macOS + Linux. Kills ONLY orphaned MCP-server processes (parent already died); also, separately (guards.reaperCodexBroker, default on), detects and lists (REPORT-ONLY, never killed) abandoned-looking openai-codex plugin app-server-broker.mjs helper processes in the log — spawned detached on purpose, so PPID 1 is normal for a LIVE one; listed only when its --cwd is gone or no live claude/codex process (excluding the broker's own descendants) has a realpath'd cwd equal to/an ancestor of/a descendant of its realpath'd --cwd, and it is past guards.reaperCodexBrokerMinAgeS (30min default). A 2026-09-25 safety review found automatic killing of this class unsafe (unquoted-space --cwd paths, /tmp vs /private/tmp symlink mismatches, the broker's own child always looking like an owner) — it stays report-only regardless of detection confidence. Install via node companion/install-reaper.js (--uninstall to remove); Windows is a documented no-op. See plugins/anti-hall/companion/README.md. |
companion/devswarm-supervisor.js (+ install-devswarm-supervisor.js) |
Interval companion (not a hook) | OPT-IN and OPTIONAL — dormant with zero effect unless DevSwarm is in use (feature-gated via devswarm-detect.js, same optionality model as the OMC/OMX integration). Detects a wedged/idle DevSwarm workspace agent from outbound activity (session transcript + git/worktree) and pokes it (an optional descriptor nudgeCommand) or escalates (log + optional escalateCommand) — never kills. Install via node companion/install-devswarm-supervisor.js (--uninstall to remove); macOS + Linux full, Windows detection-only. Workaround for claude-code#39755. v0.66.0: a cooldown-gated reconcile sweep now also runs on this existing supervisor, so stranded mesh messages self-recover instead of sitting until an update or a manual repair happens to invoke reconcile — it uses the same single-consumer lock as the drains, so it cannot race a live one. v0.93.0: hivecontrol 2.5.1's workspace list all carries no archive field, so the sweep now caches the ACTIVE set (hivecontrol-active.json) on every successful list call; a registry row absent from that cache by both id and worktree path, older than the snapshot by a 10-minute grace, reads as app-archived while the cache stays fresh (within 2x the reconcile cooldown) — liveness axis only, a genuine unread question still gates regardless. |
companion/devswarm-recover.js |
On-demand CLI (not a hook) | OPT-IN and OPTIONAL — the ONLY path in DevSwarm that ever kills a process. node companion/devswarm-recover.js <workspace-id> resolves the one confirmed wedged claude target and kill+resumes it (claude --resume), headless or interactive (naming the id is the deliberate override). Same confirm-gate safety as the old always-on supervisor. Windows: escalate-only. |
companion/lib/devswarm-store.js |
Substrate lib (not a hook) | OPTIONAL — the persistent write/derive side of the DevSwarm substrate. ONE API, TWO backends chosen by feature-detecting node:sqlite (→ WAL sqlite, else an append-only NDJSON journal — dependency-free, green on Node 18/20 through 22/24). Hooks never open the DB: it derives a summary.json projection (atomic tmp+rename) that hooks read. Tracks messages/registry/cursors + per-workspace append-only completion gates, and derives archive_ready when all required gates (configurable, default done,merged,tests_passed) are met. anti-hall stays agnostic about what any consumer gate means. (v0.70.0) New read-side filter archivedOnlyIds excludes a genuinely archived workspace (archived/<id>.json present, workspaces/<id>.json absent) from the LIVE per-turn projection immediately, without waiting for a doctor/update migration run — an archived workspace with real unread still surfaces via the orphans[] pass (no lost signal); structurally cannot hide a live row (a live workspace has its own descriptor by definition), fails open to an empty set on any read error. v0.84.0: computeSummary's orphan pass no longer reports partitions nothing can ever read — an ARCHIVED workspace with no live identity-family survivor is exactly the shape healOrphanPartitions classifies as unhealable/archived-no-family and deliberately never heals, so its unread could never drain and warned on every Primary turn forever. Those ids are now excluded via companion/lib/devswarm-orphan-policy.js (makeArchivedStrandedTest), which CALLS heal's own exported helpers rather than re-implementing the rule — tests/companion/devswarm-orphan-policy-equivalence.test.js fails CI if the two predicates ever drift. The count is preserved in a new quiet archivedStranded[], not dropped, and the classifier fails open (an unclassifiable id stays in orphans[]). v0.93.0: computeSummary now also projects archivedRegistryRows (always an array, additive) so gate/inbox callers can fold app-archived-but-still-live senders into their known-registry set without a second read. v0.94.0: resolveSenderRegistryId's final leg now delegates to devswarm-attribution.js's pickAttributionRow (pure, liveness-free) instead of the freshest-live picker, so pendingQuestions[].from can no longer flip between passes for the same stored message. |
scripts/devswarm.js (dispatcher; verb implementations in scripts/devswarm-lib/*.js) |
CLI (not a hook) | OPTIONAL — THE structured interface (CLI over MCP; stable JSON on stdout). Subcommands: register/ensure, heartbeat, inbox count\|read\|ack (the durable-inbox cursor primitive — ack is the parent-gate's non-skip clear path), inbox pull (child-side reception drain — auto-ensures the descriptor, then ONE bounded guard-safe pull: non-destructive message-count gate → at-most-one bounded read-messages, never monitor → atomic idempotent NDJSON append + store parity), inbox messages/read-primary + ack-primary --receipt (Phase 5: read-primary is read-only and returns an exact ackCommand; drain-primary-legacy keeps the one-call read-and-ack for one release) (Primary/store non-destructive read — bodies straight from the store, no descriptor needed; ack-ownership guard, 0.56.0: --ack refuses [ok:false] unless the caller's own identity, derived from cwd as ground truth, matches <id> — DEVSWARM_BUILDER_ID cannot override a different cwd-derived identity; pass --ack-as-owner for a legitimate cross-workspace ack), workspaces list, gate --set/--clear, nudge, archive (archives anti-hall's own registry state; v0.108.4: when the capability gate allows it (DevSwarm >= 2.5.3), ALSO archives the workspace in the DevSwarm app itself via hivecontrol workspace archive <id> — EXPLICIT id always, retried once on the known-flaky "Could not confirm terminal process boundary" error — returning appArchive:{attempted,ok,...}; dormant/failed falls back to an accurate manual-step instruction, never the old false "hivecontrol has no teardown command" claim; never deletes; v0.70.1: <id> also resolves an unambiguous shortId/prefix, matching the id shown in the injection/roster table — an ambiguous prefix archives nothing and lists the candidates; isSafeId still gates), archive-request (0.56.0, PARENT-side send-only — posts a [[ANTIHALL_ARCHIVE_REQUEST]] message to the child via hivecontrol workspace message-child, asking it to archive; never verifies merged/tested/deployed itself, never archives on the child's behalf), archive-ignore/archive-unignore, migrate (ANTIHALL_DEVSWARM_MIGRATE_MARK_READ=1 marks an imported legacy backlog as already-read). command-guard has a root-anchored LIGHT_EXCEPTION for it so the guard doesn't block its own wrapper. v0.61.0 mesh self-heal: drain-aware routing on send resolves to the partition a child is actually draining, plus a phantom-only rescue on the child's first mechanical self-register; new read-only diagnose (mesh-health detail: split/duplicate detection, orphans, stale partitions) and healthcheck [--json] (pass/fail, exit 0/2, for monitors/CI/the ingest daemon) verbs; register-time dedup filtered through a new isForwardable noise filter (forwards only real directs, never poke/hash-mirror junk); roster/workspaces list/diagnose are now pure reads (no summary.json write side-effect). v0.62.0: unarchive <id> (reverses archive — restores an archived descriptor + registry row); migrate-owner-keys (forward-migration backfilling/re-homing a descriptor's ownerKey, idempotent/fail-open/no-delete); reap-stale [--yes|--confirm] (dry-run-by-default reaper for descriptors verdicted stale/escalated, gated by fresh-heartbeat/recent-git-activity safety checks); reconcile-active [--active id,...] [--allow-empty] [--stdin] [--yes|--confirm] (archives every current workspace NOT in an explicit active set, dry-run by default); send --to now ALSO accepts a row's own id (the registry primary key) as a fallback when the meshId match finds nothing, and roster now prints meshId alongside id on every row — closes an addressing footgun where a value copied straight from roster used to fail closed as unregistered-recipient; reconcile runs a mis-keyed/stray-registry-row self-heal pre-pass (healRegistry) before computing its drain targets, and doctor --fix/update ALSO sweep every per-project store for this directly (AUTO-SAFE, no DevSwarm-active gate needed), idempotent and no-delete. v0.66.0: heartbeat and reconcile's aggregate ok no longer report success while a mesh broadcast failed or an individual drain target crashed/timed out — a genuinely absent hivecontrol is a benign skip, not a failure; logs now reads rotated history, not just the live file. v0.67.0: spawn sets a human-readable title after a successful hivecontrol workspace create, via a SEPARATE best-effort hivecontrol workspace update-title -b <branch> "<title>" call — the title is the caller's own -t/--title value when passed, else derived from the -p brief (first non-empty line, one leading markdown marker stripped, whitespace collapsed, full line kept — no length cap since v0.108.0); spawn's pass-through of the original argv to hivecontrol workspace create is untouched. reconcile caches whatever label hivecontrol already has for a pre-existing workspace but never invents one for a workspace with no brief on record. v0.106.0 fix: an EARLIER cut of this treated "the caller already passed -t" as "titling is handled elsewhere" and skipped the update-title follow-up entirely for that case — hivecontrol workspace create does not itself apply a title, so spawn <branch> -t "<title>" came back titled:false and the roster fell back to the raw meshId for every explicitly-titled lane. -t/--title (both spacing and = forms) is now extracted and passed to the SAME update-title follow-up as any derived title, and the local name cache (companion/lib/devswarm-names.js) is written only once hivecontrol actually confirms it — never a hopeful guess. Any lane mistitled by the earlier bug self-heals on the next reconcile sweep (the supervisor already runs one periodically): its existing read-only name backfill reads hivecontrol's own label for any workspace still missing a cached name, so no new migration was needed. Fixed a raw NUL byte (a deliberate collision-proof sentinel key, offset 81252) that made grep treat the 245KB file as binary — replaced with the \x00 escape, runtime string unchanged — and a hasFlag redeclaration collision where a new helper silently shadowed the pre-existing one and broke --yes/--confirm detection across reconcile-active and reap-stale. v0.70.1: roster now appends the same dormant hint (via companion/lib/liveness.js's isDormantActivity) that devswarm-parent-inbox.js's table uses, so the two can never disagree about which rows are still transacting. v0.70.0 mesh/store hardening: foldArchivedRegistryRows (new) folds ALL registry rows sharing an archived id's worktree (not just its own row) and picks the forward survivor by LIVENESS (pickArchiveForwardSurvivor), fixing a P0 where a real question could forward into a dead partition; ships as a dual-path migration wired into both update.js and doctor --fix's migrationFix('fold-archived-rows', ...) — idempotent, fail-open-honestly, no-delete (message rows are never deleted, only registry rows are tombstoned after their unread forwards). archive also gained a descriptor-conflict self-heal (archivedTombstoneIsOrphaned, decided by inode not registry state, fail-closed on any incomplete scan) unblocking re-archive of an id whose archived/<id>.json was a stale leftover from a prior archive generation. v0.71.0: register-primary's --session now defaults to CLAUDE_CODE_SESSION_ID (was the workspace hash), so a Primary registry row resolves its real transcript for liveness reads instead of a synthetic id nothing else recognizes. v0.74.0: gate --set merged now also runs a best-effort git-ancestry check (companion/lib/devswarm-git-truth.js's gitMergedInto, HEAD vs. the resolved default branch) and persists the verdict as a separate merged_verified gate row alongside merged — REPORT-ONLY, the merged gate is set regardless of the verdict (a squash/rebase merge legitimately breaks ancestry even though the work IS merged); a resolved-false verdict prints a stderr warning and shows as merged (unverified) on the parent roster, an unresolvable check (no default branch / spawn failure) omits merged_verified entirely. v0.75.0: inbox peek-primary (new) — the non-acking counterpart to read-primary, same message-body read, --ack forced off, for checking status without advancing the ACK cursor. v0.84.0 partition resolution follows the WORKSPACE, not the caller's cwd: inbox read-primary/inbox count resolved the store partition from the caller's working directory, so a Primary could be told to drain mail it structurally could not see; resolution now comes from the workspace's registered project via the shared registeredRepoKey helper (precedence: fresh key → recorded repoKey → non-hash ownerKey). Also: gate/ensure/archive no longer re-home a foreign project's workspace (copying messages + registry rows, rewriting ownerKey, and in archive's case removing the live descriptor) BEFORE their own ownership guard runs — a refused call now writes nothing; inbox ack <foreign-id> no longer advances the NDJSON cursor after the resolver already refused, which used to permanently skip that workspace's mail; and inbox count/read now report known:false plus the named registeredRepoKey/callerRepoKey instead of a silent zero indistinguishable from "no mail". v0.85.0 archive retires the whole identity family: archive tombstoned by <id> only, so a twin cross-linked by sessionId (one row's sessionId IS the other row's id) stayed live in workspaces/ and kept the Stop gate nagging about an inbox that could never exist. cmdArchive now retires the whole family at archive time, plus a forward migration foldArchivedFamilyDescriptors (the descriptor-file counterpart of foldArchivedRegistryRows) wired into both update and doctor's AUTO-SAFE fold-archived-family-descriptors repair — idempotent, no-delete, fail-open-honestly, grouped by the id/sessionId cross-link ONLY (never bare worktree equality, so two live tabs on one worktree are never retired). Every retire requires PROVEN write authority: an inode+bytes generation fingerprint re-read inside the per-id lock and matched against the scan-time snapshot, plus a fail-closed worktreeIsProvablyGone gate on the migration path; a mismatch or unproven gone-ness refuses and is reported in left[] (surfaced by update and by a doctor notice) instead of reading as a clean no-op. worktreePath is now persisted absolute; legacy relative values fail closed. v0.94.0 bounded reconcile + resume: reconcile now applies a total wall-clock budget (ANTIHALL_RECONCILE_BUDGET_MS, default 60000ms, or --budget-ms; 0 = unlimited) across its per-row drains — a project with dozens of stale rows previously made update.js's synchronous await hang for minutes (defect f3c1bc827d89). A row whose worktree no longer exists is skipped before it costs any budget; whatever is left when the budget runs out is deferred to a resume marker and prioritized first on the next sweep. v0.95.0: diagnose now resolves sessionId through the descriptor when the registry is stale, reporting a descriptorSessionId field on disagreement instead of surfacing the stale registry value as current; unclaimed: promotion derives the caller's real session id from --session, CLAUDE_CODE_SESSION_ID, or — only for a row still carrying the marker or lacking a sessionId — the harness's own session file found by walking the parent-pid chain (cwd-in-worktree check plus a pid-reuse/staleness liveness guard); descriptor/registry divergence is repaired in both directions, and a registry write failure during promotion is now reported as promotion.registryWriteError on inbox pull/read-primary/inbox messages output (plus a stderr line) instead of being swallowed — the next read repairs the registry from the descriptor. v0.96.0: send/fold target selection now uses a strict, heartbeat-aware liveness gate instead of a bare sessionId shape test (the fold/rehome paths are deliberately left on the older predicate); callerOwnsRow's "sole row on this worktree" ownership proof now also requires that row be unclaimed. send/heartbeat results and every ownership refusal carry an additive identity: {id, kind}. inbox ack refuses the whole verb (instead of half-acking) on a resolvable ownership mismatch — an unresolvable-caller-identity or unregistered-caller shape still fails open. diagnose rows carry an additive archivedInApp field, forcing live:false even against a fresh heartbeat; the app-side archive-cache match now also requires repositoryId agreement when both sides carry one. reconcile skips a worktree whose git root cannot resolve (skippedNotGitRoot) and checks its own wall-clock budget before that git-root probe, not just before the resulting spawn. update.js applies one overall wall-clock budget (ANTIHALL_UPDATE_POSTPULL_BUDGET_MS, default 90s) across every post-pull DevSwarm stage, deferring whole stages past the deadline. v0.100.0 (P0 safety fix): NO verb previously recognized --help/-h — --help/-h fell straight through to real dispatch, so migrate -h genuinely ran the migration and merge --help/merge -h genuinely forwarded to hivecontrol AND sent a live, unconditional mesh broadcast (a read-only-fenced diagnostic agent triggered this via a live defect report). run() now intercepts a help request (a help/-h positional anywhere, or --help) BEFORE the switch, covering every verb including the two raw-argv-tail pass-throughs (spawn/merge), with zero store opens, zero filesystem writes, zero child processes. The verb list backing top-level help output is derived from run()'s own switch statement (never hand-typed) so it cannot drift — a hand-typed list in the old default-branch error message had already drifted (reconcile-registry/wake-directive were real, dispatched verbs missing from it). Each verb's usage line names its concrete side effects when actually run. |
companion/lib/devswarm-names.js |
Substrate lib (not a hook) | OPTIONAL — new in 0.67.0. The shared fs-backed name cache behind human-readable workspace names: written by devswarm.js (spawn's update-title call, reconcile's pre-existing-workspace label cache) and read by devswarm-parent-inbox.js's status table. Atomic tmp+rename write; a read failure fails open (falls back to the raw id). |
companion/lib/devswarm-git-truth.js |
Substrate lib (not a hook) | OPTIONAL — new in 0.74.0. Two independent, fail-open git ground-truth probes for a DevSwarm child worktree: gitPushState (unpushed-commit count + whether an upstream is even configured) and gitMergeProof / gitMergedInto (HEAD-vs-REMOTE-default-branch ancestry; the one merge proof shared by gate --set merged and auto-archive gate (b)). REPORT-ONLY throughout (auto-archive only withholds an archive on it) — null on any probe failure (never a fabricated fact), same argv-array spawnSync convention as liveness.js's defaultGitCommitTs. Called from devswarm-child-turn.js (heartbeat push-state), scripts/devswarm.js's gate --set merged (merged-gate verification) and devswarm-lifecycle.js gate (b). |
companion/lib/devswarm-identity-family.js |
Substrate lib (not a hook) | OPTIONAL — the ONE owner of identity grouping; pure (no fs/store/git), the caller performs every write. familyKeyOf/collapseFamilies group by resolved worktree for COUNTING. v0.85.0 adds the mutating-side pair crossLinkedIdentity/identityFamilyTwins for archive, using a deliberately STRICTER predicate — one row's sessionId IS the other row's id — because worktree equality alone cannot justify a write (two legitimately-live tabs legitimately share one worktree). Kept in this module rather than at the archive call site so a second, parallel grouping rule cannot drift from the first. v0.93.0 adds recipientFamilyIds() so sender attribution can exclude the recipient's own identity family before ranking candidate senders. |
hooks/lib/jev-assist.js + scripts/jev-report.js |
Shared module + CLI (not a hook) | Jev metrics (v0.108.0). Each Jev call logs its real cost (the gateway's reported cost, else tokens × the owner's jev.prices table, else — v0.108.4 — tokens × the built-in jev.priceUsdPerMInput/jev.priceUsdPerMOutput default rate, never invented) and cache hits cost $0. jev-report.js shows per-integration calls, precision from tp/fp labels (jev-report.js label <hash> tp\|fp), yield, cost efficiency, overhead and a headline; changed decisions are de-duplicated by content hash; --by project\|session sums real cost per project/session too; the Vercel AI Gateway credit balance (15-min cache, report-time only). Opt-in budget watch (jev.budget.mode=watch + usdPerDay/usdPerWeek/minCreditUsd) only warns, never disables Jev. Opt-in audit snippets (jev.audit.snippets) store a redacted ~200-char snippet for decisions Jev changed; jev-report.js prune-audit trims them. |
scripts/defect.js |
CLI (not a hook) | New in 0.78.0. Durable, file-based two-way defect channel between agents running anti-hall in any repo and the anti-hall maintainer — deliberately NOT built on the mesh (bug reports about a messaging layer shouldn't travel through that layer). Subcommands: report --class C --sev p0\|p1\|p2 --sym T [...] (exits non-zero on every outcome but recorded/occurrence-appended — registry-full, occurrence-capped, defect-full, too-large, write-unverified, invalid-class, invalid-severity all fail loud), list [--mine\|--open\|--unfinished] (--open = status === 'open' only, untriaged; |
--unfinished = every status except the closed set fixed/wontfix/notabug/dup, i.e. also |
||
catches ack/partial/regressed), show <fp>, rule <fp> --status ack\|fixed\|partial\|wontfix\|notabug\|dup (maintainer-only, appends a ruling — partial carries --fixed-in/--note for a fix that shipped only in part and never derives as fully fixed; exits non-zero on anything but ruled), archive (rotation sweep: ruled-and-stale (30d) files move to archive/<YYYY-MM>/; open defects never move). Reports live in ~/.anti-hall/defects/, one append-only NDJSON file per defect, filename-fingerprinted by defect class + normalized symptom (digit/hex runs stripped) so dedup falls out of the layout. An unknown flag on any subcommand is rejected (nothing written) rather than silently dropped; only --sym-file/--repro-file take file paths. v0.84.0: over-length fields are no longer silently truncated — the result JSON and stderr now name every cut field with its original length, and the caps for note (300 → 1200), claimed, and observed were raised. |
||
hooks/lib/defect-store.js |
Shared module (not a hook) | New in 0.78.0. Backing store for scripts/defect.js and defect-nudge.js. No index, no cached state — status, occurrence count, and first/last-seen are derived from a defect file's own lines on every read; status is the LAST ruling in append order (not by timestamp), so clock skew can't flip it. Every append is re-read and matched byte-exact before reporting success; an unconfirmed write reports write-unverified and exits non-zero. Nothing is ever deleted — ack/resolve append, rotation renames into an archive dir. Bounded: ≤200 open defects, ≤20 reports/defect, 64 KiB/file, 4 KiB/line, ≤1000 archived — every cap refuses with a distinct outcome. v0.84.0: the shared clamp used to cut a field at its cap and report plain success, with nothing in the result or the stored record to show text had been lost. It now returns a truncated map naming each cut field with its original length, and text fields carry a [truncated from N chars] marker inside the persisted value. FIELD_CAPS raised (note 300 → 1200, claimed/observed → 600, repro 1200) — bounded so a maximum-length field still cannot push a record past MAX_LINE_BYTES (4096). The write itself never fails. |
foldMeshDuplicates (in scripts/devswarm-lib/fold.js) |
Migration (not a hook) | v0.61.0 — folds every prior mesh store shape (phantom rows, dual/legacy pairs, subdir-split registrations, stale entries) onto one canonical survivor per worktree, keyed by git-toplevel canonical identity (a child registered from a subdirectory now resolves to the same mesh identity as its toplevel). Idempotent, non-destructive (forward-before-tombstone; message rows are never deleted), fail-open. Wired into both update.js (runs post-update) and doctor --repair's auto-safe repair (the dry-run detect pass doubles as a read-only mesh-shape check under --check, then applies). |
companion/lib/row-state.js |
Substrate lib (not a hook) | OPTIONAL — THE one read-side row-state derivation (mesh redesign Phase 4): rowState() answers archived / app-archived / active / unknown from anti-hall's archived marker, the app-side archived-set cache and the active descriptor, in that precedence; isArchiveComplete/archiveCompleteIds answer "the archive finished". Used by routing, the roster, diagnose, the parent Stop gate, the parent-inbox table and the store's registry filter so no two surfaces can disagree. Pure reads, fail-open. |
companion/lib/migrations.js |
Migration registry (not a hook) | OPTIONAL — the ONE registry of the all-store DevSwarm forward-migrations (fold-all-stores, heal-orphan-partitions, fold-archived-rows, fold-archived-family-descriptors) and their per-version completion marker (~/.anti-hall/update-sweep-state.json), shared by doctor --repair, update and the supervisor. A marked entry is skipped with one marker read; an unmarked one gets one live scan and is stamped only on a clean finish. Deletion-class repairs are optIn and never run from it. |
companion/devswarm-migrate.js (+ devswarm-ingest.js) |
Substrate lib / daemon (not a hook) | OPTIONAL — migrate dual-reads existing on-disk state (JSON registry + legacy NDJSON inboxes) into the store: idempotent (dedupe hash), non-destructive (reads sources only — legacy files stay byte-for-byte, rollback always possible), single-consumer-locked, and count-verified before it reports success. devswarm-ingest.js = the one supervised daemon wrapping the native monitor → store; refuses to start if another monitor consumer is running (lockfile), enforcing the single-native-consumer invariant. |
companion/install-devswarm-ingest.js |
Installer (not a hook) | OPTIONAL — new in 0.54.1. Installs/refreshes devswarm-ingest.js as a CONTINUOUS supervised daemon (unlike the periodic supervisor sweep): macOS LaunchAgent with KeepAlive (re-exec on exit), Linux systemd --user .service with Restart=always (cron fallback — every minute, restart-if-dead — when systemctl is absent; up to ~60 s revive gap on a cron-only Linux host after a crash). Distinct label (com.anti-hall.devswarm-ingest) and log (~/.anti-hall/devswarm-ingest.log) from the supervisor. Idempotent; safe to install redundantly (the daemon's own single-consumer lock means only one instance ever runs). Windows: documented no-op (no pure-Node long-running user scheduler). Autonomous refresh: the update skill runs this installer's how command automatically (no offer, no ask) whenever an update happens inside an active DevSwarm session, same posture as the supervisor installer — closing the gap where the ingest daemon existed in code but nothing started it. Cwd caveat: the daemon drains the workspace of the git worktree it is INSTALLED FROM (hivecontrol resolves a workspace by cwd, not env) — the installer bakes that install-time worktree as the unit's WorkingDirectory and refuses to install if run from a non-git-worktree cwd. v0.65.0: root-caused an ENOENT storm — the daemon spawned hivecontrol by bare name while the service manager supplied only a minimal PATH, so every monitor cycle failed invisibly. The installer now discovers the binary once at install time and bakes it into the generated launchd/systemd/cron unit (never a hardcoded path); the daemon resolves it from an explicit option, ANTIHALL_DEVSWARM_HIVECONTROL, or PATH. Permanent faults (ENOENT/EACCES/ENOTDIR) now escalate through a capped backoff instead of storming the log, sliced so the heartbeat keeps writing. Orphaned ingest locks are swept on daemon start with positive-confirmation-only removal (a recycled pid or zombie holder no longer blocks restart forever; unknown holder states block by default). v0.66.0: a monitor batch that arrives but fails to parse is now logged and quarantined to disk instead of vanishing via the consume-on-read native queue (a well-formed empty result is still normal, not an error); the singleton supervisor unit now carries the same resolved hivecontrol path as the per-project units. v0.86.0: the v0.65.0/v0.66.0 fixes above were INCOMPLETE — baking the hivecontrol path fixed finding the CLI, but the emitted unit PATH never contained the directory of the ABSOLUTE node binary the unit bakes as its own interpreter, and hivecontrol is a SCRIPT whose shebang re-resolves node THROUGH PATH. The daemon started fine (absolute argv[0]) while every grandchild spawn died env: node: No such file or directory, exit 127 — 23,928 occurrences over 1,757 supervisor sweeps across three repoKeys, healed:0 on all of them. unitEnvFor now prepends dirname(execPath) and requires execPath, so a unit's PATH cannot disagree with the interpreter baked into it; install refuses if that binary is not a real file. |
Coordinator work window¶
The main thread should delegate state-changing work. coordinator-work-guard.js counts it. A Bash call is WORK when any of its segments (including commands inside sh -c, eval and $( )) is one of these:
- State-changing git: commit, am, revert, merge, rebase, cherry-pick, reset, push, pull, restore, rm, mv, stash (except
list/show), clean (except-n), apply (except--check/--stat/--numstat/--summary),switch -c,checkout -b/--,branch -D/-f, and tag create or delete. - A gh mutation. A
gh api graphqlcall counts unless it is proven to be a read. - A Bash write into a repo file that is not a notes file (the same verdict edit-guard gives an Edit tool call).
- A script-file run (
bash x.sh,python3 x.py,./x.sh). Steps run in this order and the first match wins: - an unresolvable
$VARpath counts; - an anti-hall CLI, or a script inside an anti-hall plugin root, does not count;
- a direct-exec binary (ELF or Mach-O) does not count;
- the session scratchpad, a tmp dir that is not inside a git work tree, or
.anti-hall/**counts; - a package-manager or system location does not count, whatever its mtime. That covers
/bin,/sbin,/Applications,/usrand/optoutside the repo,node_modules/.bin,.venv/bin, and the nvm, pyenv, rbenv, cargo, volta, asdf, go, dotnet and bun dirs; - inside the repo, a tracked and clean script does not count. An untracked, modified or ignored one counts. An old script that the coordinator could not edit itself does not count;
- outside the repo, scripts in
~/.local/bin,~/Libraryand~/.claude/pluginscount only if they were written or changed since the session started. Any other outside script counts.
In a non-git project the current directory acts as the root, and only a fresh or coordinator-writable script counts: an old ./configure does not, a fresh ./x.sh does.
- Inline code: python -c, perl|ruby|node -e. The body is read, never run. It counts when a literal git/gh command or file write is found next to an exec call. A precise match (a state-changing git/gh command, or a literal write into a repo file) is blockable. A loose match (a computed write target, or a quoted redirect) is counted only.
Never blocked, still counted: recovery commands (git am|rebase|cherry-pick|revert --abort|--quit, git merge --abort, git stash pop|apply) and loose inline code. Patch application is for integration agents; the coordinator has no git am exemption.
Observe-only mode: with the window above 0 and both nudgeAt and blockAt at 0, the guard only records state. Window 0 turns the guard off.
Bash edit parity: Bash writes are judged like the Edit tool: a repo file the Edit tool may not write (including gitignored outputs like build/ or .env) is blocked; write under .anti-hall/ or the scratchpad, or delegate.
Known gaps:
- Obfuscated or computed inline bodies are not detected, and loose inline matches are count-only.
- A binary compiled during the session (
go build -o /tmp/p) is never counted. - Running a tracked script that you modified this session counts. That is by design: the verify loop belongs to the subagent.
- Freshly generated, gitignored build launchers (
./build/install/app/bin/appafter a build) count. - A fresh script inside a git submodule counts, because
git statusfrom the parent fails on that path. - Deliberate mtime back-dating (
touch -t,touch -d,cp -p) hides a fresh script. - A fresh script dropped into a package-manager or system location is not counted. Neither is one in a fake managed directory made outside the repo, such as
~/x/node_modules/.bin/p.sh. - An untracked, old, non-coordinator-writable script inside a repo is not counted.
- Text scripts under
~/Libraryor~/.claude/pluginscount if they are updated mid-session (an SDK install, a plugin update). justandtaskruns,git stash pop|applylanding edits, andcd "$X" && ...with an unknown cwd are not counted.npm,npx,pnpm,yarn,bunandmakeare heavy commands and stay blocked.- A trusted
./gen.sh > docs/api.mdstays blocked by Bash edit parity. - Nudge delivery: PostToolUse
additionalContextwas live-observed delivering on Claude Code CLI 2.1.238 and is not doc-confirmed (docs/KB-claude-codex.md§1.4); re-verify after a CLI upgrade; blocks are the enforcement. Codex coordinator detection is unverified. - Scripts fed on stdin are not counted:
python3 - <<EOF,node <<EOF,sh <<EOF,echo … | sh,bash -s <<<. - Wrapper forms hide the git/gh verb:
time -p git,env -C d git,gh -R o/r pr merge,gh api -XDELETE. - Writes via
>|, a clusteredcp -rt(the wrong target is read),install,dd of=,truncate,ln -sfandrmare not judged as repo writes. git checkout .andgit checkout <file>are not counted.- With a plugin path containing
', the printed skip command itself counts as WORK.
Statusline, configuration/tuning, troubleshooting, and local testing (plugin)¶
Moved from plugins/anti-hall/README.md (v0.107.0 doc sweep).
Statusline (opt-in, one command)¶
Claude Code plugins cannot auto-apply the main statusline, so this is activated by an
installer. statusline/ ships a dispatcher whose line 1 is the rich renderer for
ANY repo (project name, git, model, context%, cost, duration, subagents). Line 1
also shows an anti-hall version chip (AH: Vx.y.z) between the
cost and email segments: ★ prefix in YELLOW for a new minor version, RED for a new
major version, plain dim when up-to-date (fail-open if no version-check cache exists).
Only if the rich renderer yields nothing does it fall back to a
monorepo-aware renderer (.gitmodules) or a simple
model | branch | dir | context% line. Line 2 is an always-on phase/context bar. No emojis.
Consolidated mode (--consolidate): pass --consolidate to merge with an existing
statusLine (e.g., the OMC HUD) instead of replacing it. The existing base is detected
from current settings or read from ANTIHALL_STATUSLINE_BASE (env), and is persisted to
~/.anti-hall/consolidated-base.json for subsequent sessions. Use this mode when you
already have another statusline and want anti-hall to extend it rather than overwrite it.
# Find the installed plugin dir and run the Node installer. Claude Code installs a
# plugin under the cache dir, versioned per marketplace/plugin
# (~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/ — for this plugin that is
# ~/.claude/plugins/cache/anti-hall/anti-hall/<version>/), but older layouts nest it
# under marketplaces/. We search all of them. A dir only counts if it contains the
# plugin manifest, so a parent dir is never mistaken for the plugin dir.
DIR=$(for d in \
~/.claude/plugins/cache/*/anti-hall/*/ \
~/.claude/plugins/cache/*/anti-hall/ \
~/.claude/plugins/cache/anti-hall/*/ \
~/.claude/plugins/cache/anti-hall/ \
~/.claude/plugins/marketplaces/*/plugins/anti-hall \
~/.claude/plugins/*/plugins/anti-hall \
~/.claude/plugins/*/anti-hall; do \
[ -f "$d/.claude-plugin/plugin.json" ] && echo "$d"; done 2>/dev/null | head -1)
[ -n "$DIR" ] && node "$DIR/statusline/install-statusline.js" || echo "anti-hall not found under ~/.claude/plugins (cache or marketplaces) — install it first (/plugin install), then re-run, or locate the dir via /plugin."
Windows is not supported (untested, dropped from CI). Use macOS or Linux.
To do it by hand, run /plugin to find the install path, then invoke the installer
directly: node "<full-path>/anti-hall/statusline/install-statusline.js".
See statusline/STATUSLINE.md for details and how to revert.
Settings (/anti-hall:settings)¶
Every user-facing anti-hall setting lives in ONE place: ~/.anti-hall/settings.json,
organized into sections (autoHandover, guards, safety, context, maintenance,
jev, jevIntegrations, limitConserve, devswarm, statusline, codexNudge,
versionAlerts, updates, defects). Since 0.108.4 every hook anti-hall registers (Claude and Codex)
has an on/off switch whose default is the old behaviour; the hook checks it first and
does nothing when it is off. The few parts with no switch on purpose (shared libraries,
bookkeeping hooks other features read) are listed with the reason at the end of
settings.js show. The declarative
registry of every setting (key, type, allowed values, default, env-var override, legacy
source, description) is hooks/lib/settings-schema.js; the read/write API is
hooks/lib/settings.js. Every devswarm knob's consumer takes an explicit env
parameter (for testability) rather than reading process.env directly; settings.js's
getWithEnv(section, key, dflt, env) threads that SAME env through and derives home
from it (never os.homedir()), so a test's isolated HOME is always honored —
tests/hygiene/settings-home-injection.test.js proves this mechanically for every
wired resolver.
/config(arrow keys, no model involved) — Claude Code's native/configpanel (v2.1.269+) shows only the headline switches (auto-handover on and threshold, Jev on, DevSwarm supervisor mode, model routing, limit conservation), the four safety guards and the API keys: 14userConfigoptions inplugin.json. Every other setting has a key in~/.anti-hall/settings.json, reachable through/anti-hall:settingsand grouped by category.userConfignever declaresoptions(a public plugin can't require v2.1.271+ just for its settings UI — declaringoptionson any field breaks plugin loading before v2.1.271, per the Claude Code plugin manifest docs), so enum switches render as a plain string field whose description lists the allowed values; older Claude Code versions still work via the skill.tests/hooks/settings-schema.test.jspins the manifest to exactly the 10headline: trueschema entries plus the 4 sensitive keys, and fails if any field declaresoptions. Every other setting that used to have a row keeps its old option name as a read-only legacy source (pluginOptionLegacy), so a value already stored under Claude Code'spluginConfigs(or still exported asCLAUDE_PLUGIN_OPTION_*) still applies, andmigrateLegacyPluginOptions(the update /doctor --repairrun of the first release that runs it) copies each non-default one into~/.anti-hall/settings.jsonwithout touching or deleting the Claude Code file (only when that stored value is already the effective one; never for the 10 headline switches, the home-only keys and the credentials; the locked safety keys are copied as human-confirmed values, the same write assettings.js set <key> <value> --confirmed, because a stored option can only be the person's own/configchoice). BothpluginConfigskey forms (anti-hall@anti-hall,anti-hall) are read. Downgrade is safe: the settings-file value outranks the stored option, so an older plugin version resolves the same effective values.- Ask for it — say "turn off the merge gate" or "set auto-handover to 80%" and the
settingsskill applies it with oneset(no table dump); "show my anti-hall settings" prints the tables only when you ask. - CLI directly:
Every subcommand takes
node plugins/anti-hall/scripts/settings.js show # every category (one table each), non-advanced settings node plugins/anti-hall/scripts/settings.js show --section jev --all # one section, including advanced/tuning knobs node plugins/anti-hall/scripts/settings.js get autoHandover.pct node plugins/anti-hall/scripts/settings.js set limitConserve.threshold 90 node plugins/anti-hall/scripts/settings.js reset limitConserve.threshold--jsonfor scripting. - Precedence (highest to lowest): an
ANTIHALL_*env var override → a value in~/.anti-hall/settings.json→ a value set via Claude Code's native/configpanel (only the 10 headline switches are declared inplugin.json'suserConfig; every other setting only reads a value stored there by an older version; shown as Sourceplugin-option) → a legacy per-feature config file (e.g.~/.anti-hall/jev.json) → the schema default. (Transitional exception,settings.jsresolveBelowFile: until the one-time legacy forward-migration is stamped for the installed plugin version, a legacyjev.jsonvalue ranks ABOVE a/configvalue, so a pre-existingjev.jsonis not masked by/config's own manifest default; after the stamp the order is as written.)show's Source column tells you which tier answered a given row. Safety keys read through this SAME chain — there is no special-cased ignore rule for them (see below). - Known limitation (
/config): a/configvalue that equals the manifest default (plugin.jsonuserConfigdefault for a headline row; the schema default for every other setting) is indistinguishable from "never set", so it counts as unset and a lower tier (a legacy file, the schema default) answers. To pin a value that equals the default, set it insettings.json(/anti-hall:settings) instead. - Safety guards need a confirmed change, not a hard refusal ("safety" in the table
below).
safety.gitGuard,safety.commandGuard,safety.editGuard,safety.swarmGuard, and the knobs that weaken them (guards.stashGuard,guards.editGuardAllow,guards.allowSubagentMailbox) are owner decision (0.108.4, revised): no hard refusal — a human direct command, or a confirmation after a clear, plain warning, is enough.settings.js setto the risky value (a guard off, a bypass on, a new allow-list path) needs--confirmed, and so does aresetwhose fallback value is the risky one (e.g. resetting an armedguards.stashGuard, whose default is off); re-arming a guard, narrowing a list, or a reset back to a safe default does not. Without it nothing changes and the call returns{ok:false, needsConfirmation:true, warning}— one short, factual, human-readable line built from the key's ownsafetyNotein the schema (calm facts, not alarming). A direct user ask to change the guard IS the confirmation; otherwise the skill shows the warning and asks (AskUserQuestionon Claude, a numbered yes/no on Codex) before applying it. Once confirmed, the value reads through the SAME precedence chain as any other setting (env > file > /config > legacy > default) — the confirmation is the protection, not an ignore rule. Turningsafety.commandGuardorsafety.editGuardoff turns off the delegation check only; command-guard's data-safety sub-guards stay on. The per-guardskip.jsonescape hatch works as before, and"all"still never covers git-guard. On Codex there is no/config: use--confirmedor the env var. A value in~/.anti-hall/settings.jsoncounts for these keys like any other; nothing mechanically stops an agent from writing that file directly — the owner chose consent over an extra guard, so the skills tell the agent never to hand-edit a safety key there to get around the confirmation. - Legacy config is never deleted.
~/.anti-hall/jev.json(and any future per-feature config file the schema maps) keeps working as a fallback forever;doctor --repairand/anti-hall:updateforward-migrate its values intosettings.jsononce (idempotent, fail-open) viacompanion/lib/migrations.js, but the legacy file is left in place. - Codex parity: the mirrored skill lives at
plugins/anti-hall/codex/skills/anti-hall-settings/SKILL.mdand drives the SAME CLI. Codex has noAskUserQuestion(the menu flow falls back to a numbered-choice prompt) and no plugin settings UI (nouserConfigequivalent in the Codex manifest) —~/.anti-hall/settings.jsonvia the skill/CLI is the only front door there, and it's the same file Claude Code's fallback path reads too.
Every setting¶
Generated from hooks/lib/settings-schema.js (a hygiene test keeps this table and the schema in sync). "adv" = advanced (shown by show --all); "safety" = a risky set/reset needs --confirmed (see above).
| Setting | Default | Env | Notes |
|---|---|---|---|
autoHandover.enabled |
true |
— | Write an automatic handover before context runs out. |
autoHandover.pct |
85 [1..99] |
ANTIHALL_AUTO_HANDOVER_PCT |
Context-usage percent that triggers an automatic handover. |
autoHandover.maxTokens |
0 [0..] |
ANTIHALL_AUTO_HANDOVER_MAX_TOKENS |
Opt-in absolute context-token ceiling that also triggers the handover, whichever of pct/maxTokens fires first; 0 (the default) = no ceiling — the real per-session window size (85% of it) is the only trigger unless a user explicitly sets this. |
autoHandover.nag |
true |
— | Nag (remind) the user when a handover is due but not yet written. |
autoHandover.nagStepPct |
5 [1..100] |
— | Percent increments between successive handover nags. |
autoHandover.nagQuietMin |
15 [1..] |
— | Minutes to wait before repeating a handover nag. |
autoHandover.gateNewWork |
true |
— | Post-handover new-work gate: once context is past the threshold and this session's handover is written, the agent sizes each new request before starting it and, above gateBudgetPct, offers to park it in the task list + handover (start after /compact or /clear) or proceed if you insist. Quick questions, the in-flight task, and DevSwarm workspace spawns pass through. |
autoHandover.gateBudgetPct |
5 [1..50] |
— | Context-window points a new request may use after the handover before the gate applies; also the one-shot backstop: one reminder (refresh the handover, offer to park the rest) once usage grows this far past where the handover was saved. |
autoHandover.decisivePrompt |
true |
— | At a turn-ending Stop point once this session's handover exists and is fresh, end the reply with one prominent line naming the exact /compact//clear//new command — or, if stale, refresh the handover first. |
autoHandover.gateHousekeepingMarkers |
'' (csv) |
— | Extra comma-separated markers (case-insensitive substrings) that identify a scheduled/cron housekeeping prompt (e.g. a mailbox-wake or DevSwarm peer-check tick), on top of the built-in defaults ("inbox tick", "peer check", "BROADCAST bug sweep") — a matching prompt never gets the post-handover new-work gate nudge. |
guards.mergeGate |
false |
ANTIHALL_MERGE_GATE |
Enable merge-readiness gate checks before merging. |
guards.shipitGate |
false |
ANTIHALL_SHIPIT_GATE |
Enable the ship-it workflow gate. |
guards.outputVerifyGuard |
true |
ANTIHALL_OUTPUT_VERIFY_GUARD |
Output-verification guard (blocks unverified completion claims). |
guards.outputVerifyOncePerTurn |
true |
ANTIHALL_OUTPUT_VERIFY_ONCE_PER_TURN |
Show the output-verify advisory once per turn per distinct pass/fail signal set instead of after every identical test re-run. false = every mixed result. |
guards.failureRootCauseNudge |
true |
ANTIHALL_FAILURE_ROOT_CAUSE_NUDGE |
Nudge toward root-cause analysis after a failure. |
guards.failureNudgeFilter |
true |
ANTIHALL_FAILURE_NUDGE_FILTER |
Cut root-cause-nudge noise: stay silent on expected exit-1 predicates (grep no-match, test, diff, git diff --quiet), on harness refusals, and after the first nudge in a turn. false = nudge on every failure again. |
guards.repoSelfDrift |
true |
ANTIHALL_REPO_SELF_DRIFT |
anti-hall's own repo-drift self-check hook. |
guards.stashGuard safety |
false |
ANTIHALL_STASH_GUARD |
SAFETY (confirm to change — see settings.js set/reset). Arm the git-stash guard in command-guard: block mutating git stash (also armed per-repo via .anti-hall/protected-stashes). |
guards.handoverCommitGuard |
true |
ANTIHALL_HANDOVER_COMMIT_GUARD |
git-guard: block a git commit whose paths include a session handover (.anti-hall/handovers/** at any depth, or HANDOVER*.md, CONTINUE-HERE.md, *.continue-here.md at the repo root). Handovers are local session state and are never committed; git add is never blocked, but git add ... && git commit in one command is checked (an ignored .anti-hall/ is not a hit); removing a tracked handover and concluding a merge/cherry-pick/rebase are allowed; fails open if git cannot be queried, and says so when a command has too many commits to check. |
guards.gitGuardHeredocData adv |
true |
ANTIHALL_GIT_GUARD_HEREDOC_DATA |
git-guard: a heredoc whose consumer is not a shell is data, so its body is not scanned as commands - cat <<EOF > notes.md, tee notes.txt <<EOF, git commit -F - <<EOF, git commit -m "$(cat <<EOF ...)", gh pr create --body-file - <<EOF. Applies only when every heredoc ends in a prose/data file (.md, .txt, .rst, .log, ...) or a git/gh message, every other command in the line is on a short allowlist (cat, tee, git, gh, echo, printf, cd, mkdir, wc, ...), and no write target is a script, extensionless file, dotfile, git hook or .git/.husky/.ssh/.config path. A body fed to bash/sh/eval/source/xargs/python/..., piped into a shell, or written to a file the same line runs stays scanned. A git beside it may use only commit/tag/notes/merge, status/log/diff/show/add or rev-parse with listed flags (no fetch/push/pull/clone/remote/submodule/config), and a gh only pr/issue/release create/edit/comment with listed flags and no --; any other flag keeps the bodies scanned. Credit trailers are checked either way. false = scan every body as shell. |
guards.gitignoreHint |
true |
ANTIHALL_GITIGNORE_HINT |
One-time (per project, every 7 days) SessionStart reminder to git-ignore .anti-hall/ when it exists in a git repo and is not ignored; doctor always reports it. |
guards.allowAnthropicEnvKey adv safety |
false |
— | SAFETY, home-settings only (no env or project override). Opt-in: let the speculation judge and Jev triage read ANTHROPIC_API_KEY from the environment. Default off: only the anthropic_api_key plugin option is used. |
guards.emitDedupe |
true |
ANTIHALL_EMIT_DEDUPE |
Deduplicate repeated hook-emit output. |
guards.gitAliasResolve adv safety |
true |
ANTIHALL_GIT_ALIAS_RESOLVE |
SAFETY (confirm to change — see settings.js set/reset). git-guard: resolve git <alias> through the repo/global git config (alias chains and !shell aliases included; loops resolve to nothing, as in git) and scan the command it really runs; block defining a git alias (git config alias.x, -c alias.x=, GIT_CONFIG_VALUE_<n>) or a shell alias x= whose body is a blocked git command; scan a call to a shell alias or function defined in the same command as the git command it forwards to. Builtin subcommands never spawn git. |
guards.gitReusedMessageCheck adv safety |
true |
ANTIHALL_GIT_REUSED_MESSAGE_CHECK |
SAFETY (confirm to change — see settings.js set/reset). git-guard: for a git commit with no -m/-F, read the message it would reuse (-C/-c/--reuse-message/--reedit-message <rev>, HEAD for --amend, -t/commit.template) and block it when it carries an AI self-credit trailer. A message reused verbatim (-C, --no-edit, a no-op editor such as true) always blocks; an editor-path commit blocks only when the command sets no real editor of its own. What an editor or commit hook writes is caught afterwards by the PostToolUse --audit. |
guards.editGuardAllow adv safety |
— | ANTIHALL_EDIT_GUARD_ALLOW |
SAFETY (confirm to change — see settings.js set/reset). Extra allowed file globs for edit-guard (comma/colon separated). |
guards.allowSubagentMailbox adv safety |
false |
ANTIHALL_ALLOW_SUBAGENT_MAILBOX |
SAFETY (confirm to change — see settings.js set/reset). One-off allow for the subagent-mailbox command pattern. |
guards.reaperMatch adv |
— | ANTIHALL_REAPER_MATCH |
Extra process-name pattern for the MCP session-end reaper. |
guards.reaperExclude adv |
— | ANTIHALL_REAPER_EXCLUDE |
Excludes matching processes from the MCP reaper. |
guards.reaperCodexBroker adv |
true |
ANTIHALL_REAPER_CODEX_BROKER |
companion/mcp-reaper.js: REPORT (never kill) abandoned openai-codex plugin app-server-broker.mjs helper processes — listed only via proof of abandonment (--cwd gone, or no live claude/codex process, excluding the broker's own descendants, has a realpath'd cwd equal to/an ancestor of/a descendant of it), never by PPID (spawned detached on purpose). |
guards.reaperCodexBrokerMinAgeS adv |
1800 [0..] |
ANTIHALL_REAPER_CODEX_BROKER_MIN_AGE_S |
Minimum age (seconds) before an abandoned-looking app-server-broker.mjs is eligible to be listed by the report-only class above (30min default; conservative since PPID gives no signal). |
guards.tasklistWorkThreshold adv |
3 [1..] |
ANTIHALL_TASKLIST_WORK_THRESHOLD |
Minimum work items before tasklist-guard fires. |
guards.pruneCompletedTasksAfter adv |
10 [1..] |
ANTIHALL_PRUNE_COMPLETED_TASKS_AFTER |
Token savings (0.117.0): once completed/cancelled tasks exceed this count, task-guard emits a one-line advisory (never a block) to prune them via TaskUpdate status=deleted after recording them in the history ledger. |
guards.progressFreshMs adv |
1800000 [0..] |
ANTIHALL_PROGRESS_FRESH_MS |
Freshness window (ms) for the progress file in tasklist-guard. |
guards.apiGuardThirdparty adv |
false |
ANTIHALL_API_GUARD_THIRDPARTY |
Also verify installed 3rd-party package APIs, not just stdlib/builtins. |
guards.modelRouting |
strict (strict/advisory/off) |
ANTIHALL_MODEL_ROUTING |
model-routing-guard (PreToolUse Agent/Task): strict blocks a mis-tiered spawn, advisory only warns, off disables the hook. |
guards.updateInSession |
true |
ANTIHALL_UPDATE_IN_SESSION |
model-routing-guard blocks a subagent spawn that runs anti-hall's own update (skills/update/scripts/update.js or run /anti-hall:update): update.js runs migrations, so /anti-hall:update runs it in the main session and command-guard allows that exact invocation. off allows delegating it. |
guards.noBlockingQuestions adv |
off (off/advise/block) |
ANTIHALL_NO_BLOCKING_QUESTIONS |
ask-guard (PreToolUse AskUserQuestion, Claude only): advise allows the question and adds the standing rule "take the recommended option, say which, continue; list destructive items as a non-blocking 'needs your OK' line"; block refuses the call unless the FIRST question's header or question text starts with DESTRUCTIVE: or CREDENTIAL: (case-sensitive; each marked call is logged to ~/.anti-hall/logs/ask-guard.ndjson). In a DevSwarm child workspace it also says to send the question to the parent. Skip name ask-guard. Skills that deliberately ask the user (settings, deadly-loop, deadly-loop-multi, ship-it, devswarm) are not rewritten: in block mode those flows must use the DESTRUCTIVE:/CREDENTIAL: marker, or turn the setting off for them. |
guards.questionAgentsNote adv |
true |
ANTIHALL_QUESTION_AGENTS_NOTE |
ask-guard (PreToolUse AskUserQuestion, Claude only): when a question is asked while one or more background agents are provably in flight (agent-scan reads the transcript), add one advisory line naming how many and their short descriptions, saying they may act on an option before the answer arrives (pause them or tell them to wait). Silent when no agent is running or the count cannot be proven (unreadable or too-long transcript). Never blocks; independent of guards.noBlockingQuestions, so it works with that set to off. Skip name ask-guard. |
guards.sharedTreeAgentNote adv |
true |
ANTIHALL_SHARED_TREE_AGENT_NOTE |
swarm-guard (PreToolUse Agent/Task): advisory when a write-capable spawn without isolation:"worktree" starts while another write-capable agent is still running in the same working tree (they can commit each other's uncommitted changes). Silent for read-only types, isolated spawns, unknown state. Never blocks. |
guards.mergeSidePickAdvisory adv |
true |
ANTIHALL_MERGE_SIDE_PICK_ADVISORY |
merge-side-pick (PostToolUse + PreToolUse Bash, Claude + Codex): advisory on a push when a conflict was resolved by taking one side wholesale (--ours/--theirs, -X ours|theirs, -s ours) and no test run (npm test, node --test, pytest, go/cargo test, ...) followed in this session. Never blocks. |
guards.modelRoutingDeployFloor |
sonnet (sonnet/opus/off) |
ANTIHALL_MODEL_ROUTING_DEPLOY_FLOOR |
model-routing-guard floor for deploy/migration/rollback/production/secret/credential-shaped spawns: at or above the floor the spawn is never blocked; below it (or with no explicit model) it gets an advisory to use at least the floor. off restores the plain routing table. |
guards.apiGuard |
true |
— | api-guard (PreToolUse Write/Edit): block fabricated stdlib/builtin APIs in written code. |
guards.speculationGuard |
true |
— | speculation-guard (Stop): block a turn that ends on unverified hedged claims. |
guards.inferenceCheck adv |
false |
ANTIHALL_INFERENCE_CHECK |
speculation-guard (Stop): also block, once per reply, a confident causal claim with no hedge word ("caused by", "the root cause is", "because", "is due to", "stems from", "this means", "the culprit is", "that's why") when no tool output, observation-tool input (a command, a path, a search pattern), pasted fenced block or task notification in the last 1 MB of the transcript mentions the stated cause. Default off: 100% precision / 95% recall on the 84-case synthetic corpus (tools/eval/inference-bench.js), but on 3,276 real final replies it flagged 3.8%, mostly design rationale ("X because Y"), so field precision is far below 0.9. See "Unsupported confident inferences" below. |
guards.claimLedger |
true |
— | claim-ledger (Stop, never blocks): record claims in the last reply that nothing in the session backs. |
guards.taskGuard |
true |
— | task-guard (Stop): block stopping while tracked tasks are still open. |
guards.tasklistGuard |
true |
— | tasklist-guard (Stop): require a task list / progress file for multi-step work. |
guards.tasklistNoTaskTools adv |
reduced (reduced/full/skip) |
ANTIHALL_TASKLIST_NO_TASK_TOOLS |
tasklist-guard nag form for a session positively known to lack task tools (today: a Codex session with no task-tool evidence in its transcript). reduced = no TaskCreate demand, list the tasks in the reply, one block per session; full = today's demand; skip = no nag. A Claude session with no evidence keeps the full demand: under claude -p the task tools are listed but deferred, so the tool list proves nothing. Evidence is structural (a TaskCreate/TaskUpdate/TodoWrite tool_use, a deferred_tools* attachment naming TaskCreate, or a task_reminder attachment), never a substring. Unset + context.protocolLevel=full = full. |
guards.stopNagBudgetPerPrompt adv |
0 |
ANTIHALL_STOP_NAG_BUDGET |
Per-prompt cap on Stop blocks from task-guard and tasklist-guard (each counted separately), inside their session caps (5 and 3). 0 = off = today's behaviour. Keyed by the Stop payload prompt_id, else the last user entry uuid; no key = not applied. |
guards.scanThrottle |
true |
ANTIHALL_SCAN_THROTTLE (deprecated alias ANTI_HALL_SCAN_THROTTLE; canonical wins) |
scan-throttle (PreToolUse Bash): advise running heavy repo-wide scans at background priority (nice/taskpolicy); never rewrites the command. |
guards.silentAgentNudge |
true |
ANTIHALL_SILENT_AGENT_NUDGE |
silent-agent-nudge (Stop): nudge once, advisory-only, when a background Agent launch has no terminal notification and a stale/missing output_file past silentAgentNudgeMin. Never kills anything. |
guards.silentAgentNudgeMin adv |
20 |
ANTIHALL_SILENT_AGENT_NUDGE_MIN |
Minutes of silence before silent-agent-nudge fires. |
guards.staleAgentStopNote adv |
true |
ANTIHALL_STALE_AGENT_STOP_NOTE |
stale-agent-stop-note (PreToolUse TaskStop, never blocks): one advisory line when TaskStop names an agent that was sent a message, or resumed, after its last report and has not reported since. Settings file + env only. |
guards.idleAgentSweep |
true |
ANTIHALL_IDLE_AGENT_SWEEP |
idle-agent-sweep (UserPromptSubmit, Claude + Codex, never blocks): once per user prompt, lists agents that finished but were never stopped (Claude: named teammates whose last report is an idle_notification with idleReason available/failed and no later SendMessage or TaskStop; Codex: multi_agent_v1 agents whose wait_agent result is completed/errored and that were never closed) and gives the exact TaskStop / close_agent call. Fires when guards.idleAgentSweepCount are idle or one has been idle guards.idleAgentSweepMin minutes. |
guards.idleAgentSweepCount adv |
3 |
ANTIHALL_IDLE_AGENT_SWEEP_COUNT |
idle-agent-sweep fires when at least this many finished agents are idle and not stopped. |
guards.idleAgentSweepMin adv |
15 |
ANTIHALL_IDLE_AGENT_SWEEP_MIN |
idle-agent-sweep also fires when any one finished agent has been idle at least this many minutes. |
guards.compactAdviceGuard |
true |
— | compact-advice-guard (Stop): block once when a reply recommends /compact at low context or within compactAdviceRecentTurns turns of a compact. |
guards.compactAdviceRecentTurns |
10 |
— | Turns after a compact boundary during which a /compact recommendation is blocked; 0 = off. |
guards.compactAdviceMarginPct adv |
10 |
— | Points below autoHandover.pct that count as low context for compact-advice-guard. |
guards.compactDeclarationGuard |
true |
— | compact-declaration-guard (PreToolUse — opt-in/default off in 0.116.0, re-enabled by default in 0.117.0): no new work in the turn after declaring SAFE TO COMPACT (reset by the next user message or a RETRACT SAFE TO COMPACT line). |
guards.injectionRepeatEvery adv |
10 [0..] |
ANTIHALL_INJECTION_REPEAT_EVERY |
Turns between full re-injections of a static UserPromptSubmit reminder block (VERIFY-FIRST, the DEVSWARM PRIMARY dispatch-tier/top-fan-out-tier suffixes) after its first-turn/post-compact copy; 0 restores every-turn injection. |
guards.codexQuotaDetect |
true |
ANTIHALL_CODEX_QUOTA_DETECT |
codex-quota-detect (PostToolUse Agent): record a Codex quota/rate-limit exhaustion seen in a codex:codex-rescue result to ~/.anti-hall/codex-availability.json so other lanes and sessions reuse it; a reset given as an ordinal date ("try again at Oct 3rd, 2026 ...") is parsed, an unparseable limit message falls back to a 6 h cooldown, and codex-nudge stays quiet while a record is live. |
guards.allowReadOnlyVerify |
true |
ANTIHALL_ALLOW_READ_ONLY_VERIFY |
command-guard narrow allow: the coordinator may run ONLY these shapes inline, each piped to tail/head/wc/grep -c/grep -m N: python3 -m pytest -q <one file>, node --test <1-2 files>, ctest -R <name>, <cc> -fsyntax-only, git clone --depth 1 <https-url> <scratchpad/tmp dir>, or a non-heavy command with --check/--dry-run/--list (the interpreter-script form is guards.allowReadOnlyVerifyScripts). No other segment left unaccounted for, no write redirect outside the scratchpad/tmp; everything else goes to a subagent. |
guards.allowReadOnlyVerifyScripts |
true |
ANTIHALL_ALLOW_READ_ONLY_VERIFY_SCRIPTS |
command-guard read-only verify, script form: <python*|node|ruby|perl|php> <existing script file> --check|--dry-run|--list piped to a bounded sink runs inline in the main thread. Inline code (-c/-e/-m/--eval), a stdin/heredoc script, wrapper verbs, env-assignment prefixes and heavy remaining arguments never qualify. Requires guards.allowReadOnlyVerify. |
guards.stopHookVersionDowngrade adv |
true |
ANTIHALL_STOP_HOOK_VERSION_DOWNGRADE |
Downgrade nudge-class Stop blocks (silent-agent-nudge, tasklist-guard, parent-gate plain NEGLECT) to advisory when installed_plugins.json registers a newer anti-hall than the running session. Never safety guards. |
guards.stopAck adv |
true |
ANTIHALL_STOP_ACK |
Honor a per-signature session ack in ~/.anti-hall/stop-ack/ for silent-agent-nudge and tasklist-guard after the user confirms a false positive. |
guards.projectCommandAllow |
true |
ANTIHALL_PROJECT_COMMAND_ALLOW |
command-guard per-project allowlist: a repo may declare its own sanctioned exact commands (e.g. a deploy script that must never be delegated) in <repo-toplevel>/.anti-hall/command-allow.json, run inline in the MAIN THREAD ONLY (never for a subagent). Default empty config means no behavior change; false disables the carve-out entirely. |
guards.projectEditAllow |
true |
ANTIHALL_PROJECT_EDIT_ALLOW |
edit-guard per-project doc-edit allowlist: <repo-toplevel>/.anti-hall/edit-allow.json ({"paths":["docs/**","PLAN.md"]}) lists repo-relative globs the MAIN THREAD may Edit/Write directly. Applies only after settings.js trust-edit-allow <repo> --confirmed records the sha256 of the file in ~/.anti-hall/trusted-edit-allow.json; any edit revokes trust; a symlinked file is refused. Never matches outside the repo, .git, .anti-hall, .claude, .codex, hook config or ~/.claude; absolute, .. and match-everything globs are ignored. Doctor reports untrusted/changed files. |
guards.allowPlainPush |
true |
ANTIHALL_ALLOW_PLAIN_PUSH |
command-guard "allow plain push": in the MAIN THREAD ONLY, lets git add/git commit/a plain git push [-u|--set-upstream] [remote] [ref] (-u/--set-upstream is the only added flag and needs BOTH an explicit remote and an explicit ref; remote omitted or a configured remote NAME from git remote — a path/URL destination never qualifies, fail-closed; ref omitted, HEAD, or the current branch only — resolved fresh via git symbolic-ref --short HEAD, fail-closed if unresolvable), and &&/; chains made up only of those three, run inline instead of being delegated. --force/-f/--force-with-lease/--force-if-includes/--mirror/--delete/-d/--all/--tags/+refspec/src:dst to another branch, any other chained segment, and pipes/redirects/subshells stay exactly as blocked as before — except three shapes: a HEAD:<current> / HEAD:refs/heads/<current> refspec (same destination as the bare form), a trailing 2>&1, and ONE final | tail [-n] N / | head [-n] N output filter. git-guard.js keeps its own independent force-push/AI-credit checks, untouched by this carve-out. |
guards.allowGcloudReads |
true |
ANTIHALL_ALLOW_GCLOUD_READS |
command-guard narrow read-only Google Cloud access: in the MAIN THREAD ONLY, lets the cloud CLI token-printing command, read-style verbs (describe, list, get-iam-policy, read) with JSON, YAML or value output into a bounded sink or jq, and a token-authorized silent HTTP GET with an optional Bearer header to an https URL on googleapis.com or a subdomain run inline. Mutating verbs, other HTTP methods, request bodies, uploads, output-to-file flags, redirects, proxies, IP literals, @file arguments and chained commands stay blocked. |
guards.allowBackgroundScratchScripts |
true |
ANTIHALL_ALLOW_BACKGROUND_SCRATCH_SCRIPTS |
command-guard background scratch scripts: in the MAIN THREAD ONLY, a Bash call with run_in_background: true may run ONE segment <interpreter> <script file> [args…] (python3, node, sh or bash) when the file is an existing regular file inside the session scratchpad or a tmp root (os.tmpdir(), /tmp, /private/tmp; realpath-checked, so a symlink out is refused). No interpreter option before the file (-c/-e/…), no env prefix or wrapper, no chaining/pipes, no $/backtick/backslash/process substitution, no stdin redirect, no write redirect outside the scratchpad/tmp. Foreground runs keep the normal rules; Monitor is unchanged. Each such run counts toward the main-thread work window (guards.coordinatorWorkWindowMinutes). |
guards.coordinatorWorkWindowMinutes adv |
10 [0..] |
ANTIHALL_COORDINATOR_WORK_WINDOW_MINUTES |
Main thread only: successful state-changing Bash calls (WORK: state-changing git, gh mutations, Bash writes into non-notes repo files, scratch/tmp/.anti-hall script runs, other text-script runs except tracked-and-clean project scripts, package-manager/system tools and old ~/.local/bin, ~/Library or ~/.claude/plugins tools, inline -c/-e code that writes files or runs state-changing git/gh) are counted over this many minutes. In a non-git project only coordinator-writable or fresh scripts count; an old script there is not WORK. Set nudgeAt and blockAt to 0 to record only. 0 = feature off. |
guards.coordinatorWorkNudgeAt adv |
4 [0..] |
ANTIHALL_COORDINATOR_WORK_NUDGE_AT |
One advisory note each time the window's WORK count reaches this. 0 = no nudge. |
guards.coordinatorWorkBlockAt adv |
7 [0..] |
ANTIHALL_COORDINATOR_WORK_BLOCK_AT |
The Nth WORK Bash call within the window is blocked. Recovery commands (git am/rebase/cherry-pick/revert --abort|--quit, merge --abort, stash pop/apply) and loosely matched inline code are counted but never blocked. Skip key coordinator-work-guard. 0 = no block. |
guards.coordinatorWorkMaxEntries adv |
50 [1..] |
ANTIHALL_COORDINATOR_WORK_MAX_ENTRIES |
Safety cap on stored window timestamps per session. |
guards.bashEditParity adv |
true |
ANTIHALL_BASH_EDIT_PARITY |
command-guard applies edit-guard's verdict to Bash writes (sed -i, perl -i, tee, cp, mv, >/>> redirects, literal python -c/node -e open-for-write paths) into repo files in the main thread. git verbs and trusted (redirect-free) project command-allow matches are never blocked by it. Also off when safety.editGuard is off or edit-guard is skipped. Both hosts (tested with Claude and Codex payload shapes). |
guards.shellWriteChecks adv |
true |
ANTIHALL_SHELL_WRITE_CHECKS |
api-guard and ship-it-guard also run on Bash (both hosts): shell-write targets go through ship-it-guard's existence gate (scratchpad/tmp-outside-a-repo writes excluded), and the visible text of a heredoc/echo/printf write into a .py/.js/.ts file through api-guard. Unparseable forms are allowed. guards.apiGuard / guards.shipitGate still switch each guard. |
guards.dispatchDemand adv |
true |
ANTIHALL_DISPATCH_DEMAND |
Per-turn DISPATCH NOW in parallel: #id "subject", … demand (task-tracker) and task-guard IDLE NEGLECT count in-flight agents PER TASK from this session's transcript (an agent whose description names #id covers it). false removes the per-turn line and restores task-guard's legacy blanket heartbeat rule. Metrics: scripts/dispatch-report.js. |
guards.taskGuardOwnerBlockedMarker adv |
true |
ANTIHALL_TASK_GUARD_OWNER_BLOCKED_MARKER |
task-guard IDLE NEGLECT: honor an explicit owner-blocked marker (metadata.blockedOn/blockedOn === owner|user|human|external, or an "OWNER:"/"OWNER DECISION" subject prefix) as non-dispatchable, instead of requiring a fake blockedBy dependency to silence the nag. false reverts to pre-marker behavior (only a real blockedBy id suppresses idle-neglect). |
guards.idleNeglectMinPriority adv |
p1 (p0/p1/p2/p3) |
ANTIHALL_IDLE_NEGLECT_MIN_PRIORITY |
task-guard IDLE NEGLECT urgency floor: a pending/unowned/unblocked task nags only when its priority is at or above (numerically ≤) this rank; anything below (e.g. P2/P3 when the floor is P1) is non-nagging backlog. Missing/unrecognized priority is always treated as P1. |
guards.maxParallelDispatch adv |
0 [0..] |
ANTIHALL_MAX_PARALLEL_DISPATCH |
Hard cap on concurrently-running background agents the DISPATCH NOW line / task-guard IDLE NEGLECT will demand up to. 0 (default) = the existing dynamic cap (min(16, cores-2)). |
guards.idleNeglectProvenOnly adv |
true |
ANTIHALL_IDLE_NEGLECT_PROVEN_ONLY |
task-guard IDLE NEGLECT blocks only when a dispatchable task is uncovered under every placement of the running agents that name no task (dispatchable > unmapped agents). The per-turn DISPATCH NOW line is unchanged. false = also block on the in_progress-first estimate. |
guards.idleNeglectAgentMaxAgeMin adv |
30 |
ANTIHALL_IDLE_NEGLECT_AGENT_MAX_AGE_MIN |
task-guard IDLE NEGLECT, proven count only: a running agent that names no task stops counting as cover once its newest sign of life (launch, SendMessage resume, pending teammate message, output-file write) is older than this many minutes, or when it was launched before the earliest uncovered task was created / last set pending or in_progress (it cannot be working on a task that did not exist). Unknown age or unknown task time = the agent still counts. 0 = never age out. |
safety.gitGuard safety |
true |
ANTIHALL_GIT_GUARD |
git-guard: block force-push and AI self-credit in commits and gh pr/issue/release bodies. |
safety.commandGuard safety |
true |
ANTIHALL_COMMAND_GUARD |
command-guard core: make the coordinator delegate heavy commands (build/test/deploy/push). Its data-safety sub-guards (DevSwarm read/send/mailbox, armed stash guard) stay on. |
safety.editGuard safety |
true |
ANTIHALL_EDIT_GUARD |
edit-guard core: make the coordinator delegate file edits outside its own plan/state/handover files. |
safety.swarmGuard safety |
true |
ANTIHALL_SWARM_GUARD |
swarm-guard: block agent spawns past the spawn-rate cap or under critical memory pressure. |
context.verifyFirstSession |
true |
— | verify-first-full (SessionStart): inject the full verify-first protocol (also re-injected after compaction). |
context.verifyFirstOrchestration |
true |
— | verify-first-orch (SessionStart): inject the orchestration discipline for the main thread. |
context.protocolLevel |
compact (compact/full) |
ANTIHALL_PROTOCOL_LEVEL |
Size of the injected verify-first and orchestration text. compact (default): a short core with every load-bearing clause inline, pointing at PROTOCOL.md; full: today's complete text on every channel, byte for byte (the one-key rollback). |
context.codexOrchFullOn |
session (session/spawn) |
ANTIHALL_CODEX_ORCH_FULL_ON |
Experimental, Codex only (>= 0.129, hooks trusted). session (default) = full orchestration rules at SessionStart, as today. spawn = SessionStart sends the compact core + compact orchestration lines and the full rules arrive once per context epoch on the first spawn_agent call (PreToolUse additionalContext, surfaced to the model by Codex 0.160). Needs positive Codex evidence, otherwise session; ignored under protocolLevel=full; a DevSwarm Primary always gets the full text at SessionStart. The spawn tool's PreToolUse tool_name is collaborationspawn_agent on Codex 0.160 (live probe, .anti-hall/plans/codex-pretooluse-context-probe.md "Spawn probe"); the hook fires and the context reaches the parent as a developer message. Settings/env only. |
context.orchFullOn |
auto (auto/spawn/session/off) |
ANTIHALL_ORCH_FULL_ON |
When the full orchestration rules A-N are sent under compact: auto (default) = session = inline at SessionStart next to the compact core; spawn = experimental, opt-in: compact lines at SessionStart and the full text once on the first Agent/Task/Workflow spawn of each context epoch, only with positive Claude evidence (--host=claude in the Claude hooks.json), otherwise coerced to session; spawn delivery is not yet verified live, so it is not the default; off = compact lines only. Ignored under full; a DevSwarm Primary always gets the full text at SessionStart. Settings/env only (no /config row). |
context.verifyFirstTurn |
true |
— | verify-first (UserPromptSubmit): the short per-turn verify-first nudge. |
context.verifyFirstSubagent |
true |
— | verify-first-subagent (SubagentStart): inject the protocol into every subagent. |
context.taskTracker |
true |
— | task-tracker (UserPromptSubmit): the task-list discipline directive and per-turn reminder. |
context.handoverResume |
true |
— | handover-resume (SessionStart): point a fresh or compacted session at the newest handover. |
context.defectNudge |
true |
— | defect-nudge (SessionStart): the once-a-day note about the defect channel. |
context.dedupeWindowMin adv |
20 [0..] |
ANTIHALL_DEDUPE_WINDOW_MIN |
Fallback per-session suppression window (minutes) for repeated UserPromptSubmit injection blocks (LIMIT CONSERVATION, TASK-LIST, DEVSWARM COMMS OVERRIDE, DEVSWARM WORKSPACES) when a burst of queued prompts is delivered in one turn and the transcript cannot confirm the earlier copy was already read; content that changed always re-emits. 0 disables emit-dedupe entirely (same as guards.emitDedupe=false). Suppression counts surface in /anti-hall:doctor. |
context.injectGate |
true |
ANTIHALL_INJECT_GATE |
Master switch of the engine's injection gate: the hooks that re-send the same context every turn (limit conservation, task-tracker, the DevSwarm comms-override line, swarm-guard's shared-tree advisory) are injected only when the model does not already hold it. Off: every hook's output passes through unchanged. Counters: ah-engine metrics (inject_*), per session ah-engine ctl gate. |
context.roleGuard |
true |
ANTIHALL_ROLE_GUARD |
Refuses an ah-engine verb the caller's role may not run (role matrix in engine/defaults/roles.toml: owner-level verbs main session only, a workspace child acts on itself only); applies to the PreToolUse Bash check and the command line. |
context.roleNote |
true |
ANTIHALL_ROLE_NOTE |
SessionStart / SubagentStart note telling the session its role and the engine verbs it may use, pointing at the anti-hall:engine skill. |
context.injectGateLimit |
true |
ANTIHALL_INJECT_GATE_LIMIT |
Cut 1: the limit-conservation directive on a usage-band or reset-window change, else a short keepalive. |
context.injectGateLimitEvery adv |
10 [1..] |
ANTIHALL_INJECT_GATE_LIMIT_EVERY |
Turns between keepalives of an unchanged limit-conservation directive. |
context.injectGateTask |
true |
ANTIHALL_INJECT_GATE_TASK |
Cut 2: task-tracker's long form always passes; its short reminder and an unchanged freshness note pass every N turns. |
context.injectGateTaskEvery adv |
10 [1..] |
ANTIHALL_INJECT_GATE_TASK_EVERY |
Turns between short task-tracker reminders and unchanged freshness notes. |
context.injectGateComms |
true |
ANTIHALL_INJECT_GATE_COMMS |
Cut 3: the DevSwarm comms-override line and the workspace-title instruction once per session, when changed, and as a keepalive. |
context.injectGateCommsEvery adv |
30 [1..] |
ANTIHALL_INJECT_GATE_COMMS_EVERY |
Turns between keepalives of the unchanged comms-override line. |
context.injectGateSwarm |
true |
ANTIHALL_INJECT_GATE_SWARM |
Cut 4: swarm-guard's shared-tree advisory when new or changed, and again only after N turns. |
context.injectGateSwarmEvery adv |
20 [1..] |
ANTIHALL_INJECT_GATE_SWARM_EVERY |
Turns between repeats of an unchanged shared-tree advisory. |
maintenance.repairOnReload |
true |
ANTIHALL_REPAIR_ON_RELOAD |
repair-on-reload (SessionStart/UserPromptSubmit): re-apply safe doctor repairs after a plugin update. |
maintenance.progressPrune |
true |
— | progress-prune (SessionStart): archive stale per-session progress files into the history ledger. |
maintenance.precompactSnapshot |
true |
— | precompact-snapshot (PreCompact): write a mechanical continuation snapshot before compaction. |
maintenance.taskLifecycleLog |
true |
— | task-lifecycle-log (TaskCreated/TaskCompleted): append task events to the per-session history ledger. |
maintenance.sessionEndReaper |
true |
ANTIHALL_SESSION_END_REAPER (deprecated alias ANTI_HALL_SESSION_END_REAPER; canonical wins) |
session-end-mcp-reaper (SessionEnd): kill orphaned MCP-server processes this session left behind. |
agents.tracker |
true |
ANTIHALL_AGENT_TRACKER |
agent tracker (engine job agent_tick): follow every agent, raise hung / looping / token-waste / drift / stale-heartbeat / no-wake-path signals; off, a tick does nothing. |
agents.reminders |
true |
ANTIHALL_AGENT_REMINDERS |
agent-reminders (UserPromptSubmit, PostToolUse): deliver the tracker's queued reminders to the agent that owns them; off, signals are recorded but nothing is queued. |
agents.ownerNotify |
false |
ANTIHALL_AGENT_OWNER_NOTIFY |
agent tracker owner notices: also append hung / looping / token-waste advisories to the owner notices file. |
versionAlerts.antiHall |
true |
ANTIHALL_VERSION_ALERT |
Alert when a newer anti-hall version is available. |
versionAlerts.claudeCli |
true |
ANTIHALL_CLAUDE_CLI_VERSION_ALERT |
Alert when a newer Claude CLI version is available. |
versionAlerts.devswarm |
true |
ANTIHALL_DEVSWARM_VERSION_ALERT |
Alert when a newer DevSwarm/hivecontrol version is available. |
updates.quiet |
false |
ANTIHALL_UPDATE_QUIET |
Suppress update output (for scripted capture). |
updates.allowCachePrune |
true |
ANTIHALL_ALLOW_CACHE_PRUNE |
Enables the opt-in doctor --prune-cache verb. Never automatic (not run by update.js, the supervisor, a cron, SessionStart or any hook). Without --confirmed it only lists the old ~/.claude/plugins/cache/anti-hall/anti-hall/<semver>/ dirs it would remove and their total size; --confirmed removes them and logs each removal. Always keeps the newest 3, the installPath registered in installed_plugins.json, every version a live process runs from (process cwd or argv), the running version, and anything unparseable. Symlinks and paths outside that root are refused; if the live-process scan is unavailable nothing is removed. false disables the verb. |
updates.reconcileBudgetMs adv |
60000 [0..] |
ANTIHALL_RECONCILE_BUDGET_MS |
Time budget (ms) for the reconcile step during update; 0 = unlimited. |
updates.postpullBudgetMs adv |
90000 [0..] |
ANTIHALL_UPDATE_POSTPULL_BUDGET_MS |
Time budget (ms) for the post-pull update sweep; 0 = unlimited. |
updates.sweepBudgetMs adv |
20000 [0..] |
ANTIHALL_UPDATE_SWEEP_BUDGET_MS |
Overall time budget (ms) for the update sweep. |
limitConserve.mode |
auto (auto/on/off) |
ANTIHALL_LIMIT_CONSERVE |
Force conservation mode on/off, or auto-detect from the OMC usage cache. |
limitConserve.threshold |
85 [1..99] |
ANTIHALL_LIMIT_THRESHOLD |
Usage percent that triggers conservation mode. |
limitConserve.accountCheck adv |
true |
ANTIHALL_LIMIT_ACCOUNT_CHECK |
Guard against stale usage-cache readings after an account switch. |
jev.enabled |
false |
ANTIHALL_JEV |
Enable Jev (ANTIHALL_JEV=0 always force-disables regardless of this). |
jev.transport |
vercel (vercel/typesafe) |
— | Vercel AI Gateway passthrough (default) or a direct TypeSafe API call. |
jev.fallbackTransport |
none (none/vercel/typesafe) |
— | Automatic backup vendor: when the primary transport times out, has a network error, returns 5xx (incl. 529), 402 or 429 (or a 400/403 naming insufficient balance), ONE retry goes to this transport inside the same time budget; 401/403 and other 4xx never fall back (a bad primary key must surface). Equal to jev.transport = off. Needs its OWN vendor-bound key (plugin option jev_vercel_api_key / jev_typesafe_api_key, or that vendor's key file with jev.allowLegacyKeyRead; keys are never sent to another vendor) and a per-vendor circuit breaker skips a vendor for 5 min after 3 consecutive eligible failures (both open = Jev skipped, no double timeouts). NOT full redundancy: both routes very likely reach the same TypeSafe model (inferred from the model ids and identical answers on a 40-item test, unconfirmed), so it covers the direct account's balance/quota or an endpoint outage, probably not a model outage; the guards then use their built-in rules as when Jev is off. With a fallback on, decision text can reach the second vendor. Decision rows record transport and fellBack. Set with jev-setup.js enable --fallback <vercel\|typesafe\|none> or settings.js set jev.fallbackTransport <value>. |
jev.judgeModel |
haiku |
ANTIHALL_JUDGE_MODEL |
Model alias used for speculation-judge / jev-triage LLM calls (an alias, never a pinned version: the CLI and API resolve it to the latest model). |
jev.judgeBackend adv |
api (api/cli/auto) |
ANTIHALL_JUDGE_BACKEND |
How speculation-judge reaches the model. api = Anthropic API with the anthropic_api_key plugin option; cli = the local claude -p CLI on your own Claude login, no API key (no tools, no MCP servers, no settings files, all hooks disabled; about 5–6 s per turn end, measured); auto = api when a key is visible, else cli. Fail-open in every mode. |
jev.speculationBackend adv |
haiku (haiku/jev/cascade) |
ANTIHALL_JEV_SPECULATION_BACKEND |
Which backend answers the semantic speculation question when jev.semanticJudge is on. haiku = the judge asks the model (jev.judgeModel through jev.judgeBackend), except while Jev's own speculation integration is on; cascade = as jev, with the Jev-first cascade switched on for speculation; jev = the judge never asks the model and speculation-guard's Jev path is the only semantic check. Answered by the engine; the Node fallback keeps haiku. |
jev.triageBackend adv |
jev (jev/haiku/cascade) |
ANTIHALL_JEV_TRIAGE_BACKEND |
Which backend labels mesh messages in ah-engine jev triage. jev = Jev first, the Anthropic API fills a missing label when a key is visible (the Node worker's behaviour); cascade = as jev, then a label Jev left open is re-judged by the model shown Jev's answer; haiku = the model alone through jev.judgeBackend (the local claude -p takes about 3–4 s per message, so the triage budget must allow it). |
jev.cascade adv |
true |
ANTIHALL_JEV_CASCADE |
Global kill switch of the Jev-first cascade: false means no Jev answer is ever re-judged by the model, whatever the per-integration jevCascade.* switches say. |
jev.cascadeShowJevAnswer adv |
true |
ANTIHALL_JEV_CASCADE_SHOW_JEV_ANSWER |
Whether the model that re-judges an unsure Jev answer is shown Jev's answer and confidence (true) or only the evidence (false), so anchoring can be A/B tested. |
jev.semanticJudge |
false |
ANTIHALL_SEMANTIC_JUDGE |
Enable the semantic speculation-judge hook (off = hook no-ops). |
jev.allowLegacyKeyRead adv safety |
false |
— | SAFETY, home-settings only (~/.anti-hall/settings.json; no env or project override). Opt-in: read the Jev key from AI_GATEWAY_API_KEY / TYPESAFE_API_KEY env vars and the key file. Default off: only the jev_api_key plugin option is used. Needed for background tools and Codex (see "Where a stored key is visible"). |
jev.genericKeyVendor adv safety |
vercel (vercel/typesafe) |
— | SAFETY, home-settings only (~/.anti-hall/settings.json; no env, /config or legacy-file route). The ONE vendor the legacy generic jev_api_key plugin option and jev.keyFile are bound to: they carry no vendor name, so they are never sent to any other vendor, whatever jev.transport / jev.fallbackTransport say (jev.transport does NOT decide this, because enable --transport rewrites it). Vendor-named keys (jev_vercel_api_key, jev_typesafe_api_key) need no binding. Change only with jev-setup.js bind-generic-key --vendor <v> (a deliberate human command). An existing install with a typesafe transport and a generic key is never auto-bound: it stays on vercel, so the generic key is refused for typesafe, and a one-time notice says how to bind or to enter a typesafe key. |
jev.keyFile adv |
— | — | Credential key-file path (default depends on transport). |
jev.timeoutMs adv |
1500 [1..3000] |
— | Per-call timeout (ms), capped at 3000. |
jev.confidenceThreshold adv |
0.85 [0..1] |
— | Minimum confidence for a Jev answer to be trusted by callers. |
jev.triage adv |
true |
— | Message-triage labeling once Jev is enabled. |
jev.triageUrgentThreshold adv |
0.9 [0..1] |
— | Confidence threshold for the urgent triage label. |
jev.budget.mode |
unlimited (unlimited/watch) |
— | Jev spend: no limit, or warn when over budget (never auto-disables). |
jev.budget.usdPerDay |
— (>0) | — | optional: daily USD spend threshold, used only when budget.mode=watch. |
jev.budget.usdPerWeek |
— (>0) | — | optional: weekly USD spend threshold, used only when budget.mode=watch. |
jev.weeklyNotice |
true |
— | Once-a-week SessionStart scorecard notice naming one integration worth promoting or turning off (Jev enabled only). |
jev.audit.snippets adv |
false |
ANTIHALL_JEV_AUDIT_SNIPPETS |
Store a redacted ~200-char snippet for decisions Jev changed (off by default: privacy). |
jev.logRotatedFiles adv |
10 |
— | Rotated generations kept for jev-assist.ndjson (2MB each) and jev-triage.ndjson (1MB each); 10 ≈ 20 days of decision rows. |
jev.rollupRetentionDays adv |
0 |
— | Days of daily rollups (~/.anti-hall/logs/jev-daily/<day>.json) to keep; 0 keeps all, only an explicit N > 0 removes older ones. |
jev.budget.minCreditUsd |
— (>0) | — | optional: warn (once a day, budget.mode=watch only) when the gateway credit balance drops below this USD amount. |
jevIntegrations.speculation |
on (on/shadow/off) |
— | Is this claim unsupported speculation (add-block trust). |
jevIntegrations.triage |
on (on/shadow/off) |
— | Mesh message urgency/kind labeling (advisory trust). |
jevIntegrations.newRequest |
shadow (on/shadow/off) |
— | Classify a prompt as new-request/follow-up/correction/question (advisory trust). |
jevIntegrations.claimLedger |
shadow (on/shadow/off) |
— | Is a flagged claim genuinely unsupported by evidence (relax-block trust). |
jevIntegrations.outputVerifyGuard |
shadow (on/shadow/off) |
— | Does this test-runner output actually indicate a pass (advisory trust). |
jevIntegrations.gitGuardSelfCredit |
shadow (on/shadow/off) |
— | Does this commit/PR message contain paraphrased AI self-credit (add-block trust; never relaxes git-guard). |
jevIntegrations.modelRouting |
shadow (on/shadow/off) |
— | Is this agent-spawn task actually mechanical (relax-block trust). |
jevIntegrations.tasklistTrivial |
shadow (on/shadow/off) |
— | tasklist-guard: is this session a genuinely non-trivial, multi-part effort (relax-block trust; 1.5 s cap, fail-open). |
jevIntegrations.codexNudgeSubstantial |
shadow (on/shadow/off) |
— | codex-nudge: are these file edits genuinely substantial, not just formatting (relax-block trust; 1.5 s cap, fail-open). |
jevIntegrations.mergeGateHedge |
shadow (on/shadow/off) |
— | Does this text hedge on merge-readiness (relax-block trust; askDetached fire-and-forget in shadow). |
jevIntegrations.parentGateQuestion |
shadow (on/shadow/off) |
— | Is this unread child message really a question awaiting a reply (add-block trust; cache-only, zero network). |
jevIntegrations.supervisorBlockerLabel |
shadow (on/shadow/off) |
— | Is a stale child waiting-on-parent or genuinely wedged (advisory trust; cache-only, zero network). |
jevIntegrations.findingDedup |
on (on/shadow/off) |
— | Do two deadly-loop TRIO findings describe the same underlying issue, for advisory duplicate-grouping (advisory trust); 65/65 correct at confidence ≥0.85 on a 30-day, 3-project offline benchmark. |
jevIntegrations.postHandoverGate |
off (on/shadow/off) |
— | Does this new request fit in the remaining post-handover context budget (advisory trust; askDetached fire-and-forget). Default off: an offline benchmark (n=299) found park-recall 17.6% vs 28.8% for the agent's own size judgment plus the measured budget backstop, no gain over that baseline. |
jevIntegrations.speculationFramed |
shadow (on/shadow/off) |
— | When a deterministic speculation-guard hedge hit sits under a FRAMED heading/line prefix (Expected/Plan/"Should be \<verb>:"/Unverified/"not yet measured"), is it a stated expectation/plan or an unverified claim presented as fact (relax-block trust: Jev may only turn the framed hit's block into a non-block; an unframed hedge never consults Jev and always blocks). |
jevIntegrations.dispatchTier |
on (on/shadow/off) |
— | Recommend workspace / workflow / subagent per task on the DISPATCH NOW line (advisory; never blocks; the Primary makes the final call). Asked askDetached when a task's text changes. Metrics: scripts/dispatch-report.js. |
jevIntegrations.devswarmOnBrief |
on (on/shadow/off) |
— | DevSwarm supervision recommendation: is a child's off-scope work off its brief. Annotates the off-scope warning; never suppresses, blocks or kills. shadow = logged only. |
jevIntegrations.devswarmExtraSanctioned |
on (on/shadow/off) |
— | Recommendation: did the user ask for this off-scope work (the child's recent prompts, secrets scrubbed; threshold 0.9). An explicit scope add always wins. |
jevIntegrations.devswarmWaitKind |
on (on/shadow/off) |
— | Recommendation: is an idle/stalled child stuck, or waiting on CI, the owner or a peer. Annotates the idle/stall warning. |
jevIntegrations.devswarmLoop |
on (on/shadow/off) |
— | Recommendation: is a busy child looping on its step (input includes the burn figure; threshold 0.9). Annotates a burn/stall warning, or adds an advisory loop warning. |
jevIntegrations.devswarmStepMap |
on (on/shadow/off) |
— | Recommendation: which step a heartbeat summary without --step describes (threshold 0.8); shows as ~N only while the child never reported a step. |
jevCascade.speculation adv |
off (on/off) |
ANTIHALL_JEV_CASCADE_SPECULATION |
Re-judge a speculation Jev answer with the model when Jev's confidence is under the integration's escalation threshold (engine/defaults/judge.toml, default its act threshold). A hook-blocking decision never waits: Jev's answer applies now, the re-judged one from the next ask; a background caller waits. One telemetry row per escalation in logs/judge-calls.ndjson. |
jevCascade.triage adv |
off (on/off) |
ANTIHALL_JEV_CASCADE_TRIAGE |
Re-judge a triage Jev answer with the model when Jev's confidence is under the integration's escalation threshold (engine/defaults/judge.toml, default its act threshold). A hook-blocking decision never waits: Jev's answer applies now, the re-judged one from the next ask; a background caller waits. One telemetry row per escalation in logs/judge-calls.ndjson. |
jevCascade.newRequest adv |
off (on/off) |
ANTIHALL_JEV_CASCADE_NEW_REQUEST |
Re-judge a newRequest Jev answer with the model when Jev's confidence is under the integration's escalation threshold (engine/defaults/judge.toml, default its act threshold). A hook-blocking decision never waits: Jev's answer applies now, the re-judged one from the next ask; a background caller waits. One telemetry row per escalation in logs/judge-calls.ndjson. |
jevCascade.claimLedger adv |
off (on/off) |
ANTIHALL_JEV_CASCADE_CLAIM_LEDGER |
Re-judge a claimLedger Jev answer with the model when Jev's confidence is under the integration's escalation threshold (engine/defaults/judge.toml, default its act threshold). A hook-blocking decision never waits: Jev's answer applies now, the re-judged one from the next ask; a background caller waits. One telemetry row per escalation in logs/judge-calls.ndjson. |
jevCascade.outputVerifyGuard adv |
off (on/off) |
ANTIHALL_JEV_CASCADE_OUTPUT_VERIFY_GUARD |
Re-judge a outputVerifyGuard Jev answer with the model when Jev's confidence is under the integration's escalation threshold (engine/defaults/judge.toml, default its act threshold). A hook-blocking decision never waits: Jev's answer applies now, the re-judged one from the next ask; a background caller waits. One telemetry row per escalation in logs/judge-calls.ndjson. |
jevCascade.gitGuardSelfCredit adv |
off (on/off) |
ANTIHALL_JEV_CASCADE_GIT_GUARD_SELF_CREDIT |
Re-judge a gitGuardSelfCredit Jev answer with the model when Jev's confidence is under the integration's escalation threshold (engine/defaults/judge.toml, default its act threshold). A hook-blocking decision never waits: Jev's answer applies now, the re-judged one from the next ask; a background caller waits. One telemetry row per escalation in logs/judge-calls.ndjson. |
jevCascade.modelRouting adv |
off (on/off) |
ANTIHALL_JEV_CASCADE_MODEL_ROUTING |
Re-judge a modelRouting Jev answer with the model when Jev's confidence is under the integration's escalation threshold (engine/defaults/judge.toml, default its act threshold). A hook-blocking decision never waits: Jev's answer applies now, the re-judged one from the next ask; a background caller waits. One telemetry row per escalation in logs/judge-calls.ndjson. |
jevCascade.tasklistTrivial adv |
off (on/off) |
ANTIHALL_JEV_CASCADE_TASKLIST_TRIVIAL |
Re-judge a tasklistTrivial Jev answer with the model when Jev's confidence is under the integration's escalation threshold (engine/defaults/judge.toml, default its act threshold). A hook-blocking decision never waits: Jev's answer applies now, the re-judged one from the next ask; a background caller waits. One telemetry row per escalation in logs/judge-calls.ndjson. |
jevCascade.codexNudgeSubstantial adv |
off (on/off) |
ANTIHALL_JEV_CASCADE_CODEX_NUDGE_SUBSTANTIAL |
Re-judge a codexNudgeSubstantial Jev answer with the model when Jev's confidence is under the integration's escalation threshold (engine/defaults/judge.toml, default its act threshold). A hook-blocking decision never waits: Jev's answer applies now, the re-judged one from the next ask; a background caller waits. One telemetry row per escalation in logs/judge-calls.ndjson. |
jevCascade.mergeGateHedge adv |
off (on/off) |
ANTIHALL_JEV_CASCADE_MERGE_GATE_HEDGE |
Re-judge a mergeGateHedge Jev answer with the model when Jev's confidence is under the integration's escalation threshold (engine/defaults/judge.toml, default its act threshold). A hook-blocking decision never waits: Jev's answer applies now, the re-judged one from the next ask; a background caller waits. One telemetry row per escalation in logs/judge-calls.ndjson. |
jevCascade.parentGateQuestion adv |
off (on/off) |
ANTIHALL_JEV_CASCADE_PARENT_GATE_QUESTION |
Re-judge a parentGateQuestion Jev answer with the model when Jev's confidence is under the integration's escalation threshold (engine/defaults/judge.toml, default its act threshold). A hook-blocking decision never waits: Jev's answer applies now, the re-judged one from the next ask; a background caller waits. One telemetry row per escalation in logs/judge-calls.ndjson. |
jevCascade.supervisorBlockerLabel adv |
off (on/off) |
ANTIHALL_JEV_CASCADE_SUPERVISOR_BLOCKER_LABEL |
Re-judge a supervisorBlockerLabel Jev answer with the model when Jev's confidence is under the integration's escalation threshold (engine/defaults/judge.toml, default its act threshold). A hook-blocking decision never waits: Jev's answer applies now, the re-judged one from the next ask; a background caller waits. One telemetry row per escalation in logs/judge-calls.ndjson. |
jevCascade.findingDedup adv |
off (on/off) |
ANTIHALL_JEV_CASCADE_FINDING_DEDUP |
Re-judge a findingDedup Jev answer with the model when Jev's confidence is under the integration's escalation threshold (engine/defaults/judge.toml, default its act threshold). A hook-blocking decision never waits: Jev's answer applies now, the re-judged one from the next ask; a background caller waits. One telemetry row per escalation in logs/judge-calls.ndjson. |
jevCascade.postHandoverGate adv |
off (on/off) |
ANTIHALL_JEV_CASCADE_POST_HANDOVER_GATE |
Re-judge a postHandoverGate Jev answer with the model when Jev's confidence is under the integration's escalation threshold (engine/defaults/judge.toml, default its act threshold). A hook-blocking decision never waits: Jev's answer applies now, the re-judged one from the next ask; a background caller waits. One telemetry row per escalation in logs/judge-calls.ndjson. |
jevCascade.speculationFramed adv |
off (on/off) |
ANTIHALL_JEV_CASCADE_SPECULATION_FRAMED |
Re-judge a speculationFramed Jev answer with the model when Jev's confidence is under the integration's escalation threshold (engine/defaults/judge.toml, default its act threshold). A hook-blocking decision never waits: Jev's answer applies now, the re-judged one from the next ask; a background caller waits. One telemetry row per escalation in logs/judge-calls.ndjson. |
jevCascade.dispatchTier adv |
off (on/off) |
ANTIHALL_JEV_CASCADE_DISPATCH_TIER |
Re-judge a dispatchTier Jev answer with the model when Jev's confidence is under the integration's escalation threshold (engine/defaults/judge.toml, default its act threshold). A hook-blocking decision never waits: Jev's answer applies now, the re-judged one from the next ask; a background caller waits. One telemetry row per escalation in logs/judge-calls.ndjson. |
jevCascade.devswarmOnBrief adv |
off (on/off) |
ANTIHALL_JEV_CASCADE_DEVSWARM_ON_BRIEF |
Re-judge a devswarmOnBrief Jev answer with the model when Jev's confidence is under the integration's escalation threshold (engine/defaults/judge.toml, default its act threshold). A hook-blocking decision never waits: Jev's answer applies now, the re-judged one from the next ask; a background caller waits. One telemetry row per escalation in logs/judge-calls.ndjson. |
jevCascade.devswarmExtraSanctioned adv |
off (on/off) |
ANTIHALL_JEV_CASCADE_DEVSWARM_EXTRA_SANCTIONED |
Re-judge a devswarmExtraSanctioned Jev answer with the model when Jev's confidence is under the integration's escalation threshold (engine/defaults/judge.toml, default its act threshold). A hook-blocking decision never waits: Jev's answer applies now, the re-judged one from the next ask; a background caller waits. One telemetry row per escalation in logs/judge-calls.ndjson. |
jevCascade.devswarmWaitKind adv |
off (on/off) |
ANTIHALL_JEV_CASCADE_DEVSWARM_WAIT_KIND |
Re-judge a devswarmWaitKind Jev answer with the model when Jev's confidence is under the integration's escalation threshold (engine/defaults/judge.toml, default its act threshold). A hook-blocking decision never waits: Jev's answer applies now, the re-judged one from the next ask; a background caller waits. One telemetry row per escalation in logs/judge-calls.ndjson. |
jevCascade.devswarmLoop adv |
off (on/off) |
ANTIHALL_JEV_CASCADE_DEVSWARM_LOOP |
Re-judge a devswarmLoop Jev answer with the model when Jev's confidence is under the integration's escalation threshold (engine/defaults/judge.toml, default its act threshold). A hook-blocking decision never waits: Jev's answer applies now, the re-judged one from the next ask; a background caller waits. One telemetry row per escalation in logs/judge-calls.ndjson. |
jevCascade.devswarmStepMap adv |
off (on/off) |
ANTIHALL_JEV_CASCADE_DEVSWARM_STEP_MAP |
Re-judge a devswarmStepMap Jev answer with the model when Jev's confidence is under the integration's escalation threshold (engine/defaults/judge.toml, default its act threshold). A hook-blocking decision never waits: Jev's answer applies now, the re-judged one from the next ask; a background caller waits. One telemetry row per escalation in logs/judge-calls.ndjson. |
jev.prices adv |
— | — | computed: per-model USD price table {model: {inPerMTok, outPerMTok}} (or a "default" entry), used only when the gateway reports tokens but no cost. File-only (no env, no CLI set) — edit ~/.anti-hall/settings.json directly. |
jev.priceUsdPerMInput adv |
0.042 [0..] |
ANTIHALL_JEV_PRICE_USD_PER_M_INPUT |
USD per 1M input tokens for the Jev judge call, used to compute costUsd when the gateway reports tokens but no cost and jev.prices has no matching entry — Jev's own published rate by default. |
jev.priceUsdPerMOutput adv |
0 [0..] |
ANTIHALL_JEV_PRICE_USD_PER_M_OUTPUT |
USD per 1M output tokens for the Jev judge call (default 0 — output is free on the verified rate). |
jev.dispatchTierNoWorkspaceRepos adv |
'' |
ANTIHALL_JEV_DISPATCH_TIER_NO_WORKSPACE_REPOS |
Repos (basenames or absolute paths; * = all) where dispatchTier never recommends workspace (shown as subagent). |
jev.dispatchTierDetectNoWorkspaces adv |
true |
ANTIHALL_JEV_DISPATCH_TIER_DETECT_NO_WORKSPACES |
Also treat a repo as no-workspace when its CLAUDE.md/AGENTS.md says "no workspaces for real work". |
jev.reviewAfterDays |
7 [1..365] |
ANTIHALL_JEV_REVIEW_AFTER_DAYS |
Minimum days an integration must have sat in shadow mode before its shadow numbers are due for owner review (also the re-review cadence once reviewed). |
jev.reviewMinDecisions |
30 [0..] |
ANTIHALL_JEV_REVIEW_MIN_DECISIONS |
Minimum decisions logged for a shadow integration before its review is due — avoids nagging about a barely-used integration with too little data to judge. |
jev.recommendNoticeHeadless adv |
false |
ANTIHALL_JEV_NOTICE_HEADLESS |
Allow the "Recommended: enable Jev" notice in non-interactive runs (claude -p/SDK, detected by CLAUDE_CODE_ENTRYPOINT=sdk-*; sdk-cli verified live). Default false: a headless run neither shows it nor uses up its once-per-30-days slot. JEV REVIEW DUE is unaffected; Codex unchanged. Unset + context.protocolLevel=full = true. |
jev.recommendNotice |
true |
ANTIHALL_JEV_RECOMMEND_NOTICE |
Bold "Recommended: enable Jev" SessionStart/doctor notice, shown only while Jev is NOT enabled (once on first install, then at most every 30 days). Set false to silence it. |
jev.reviewReminder |
true |
ANTIHALL_JEV_REVIEW_REMINDER |
Durable "time to review the Jev shadow numbers" SessionStart/doctor nudge (on by default — owner opt-out only). |
devswarm.hivecontrol |
— | ANTIHALL_DEVSWARM_HIVECONTROL |
Explicit path to the hivecontrol CLI binary (default: PATH lookup — no single default value; empty means "look it up"). |
devswarm.supervisorMode |
auto (auto/on/off) |
ANTIHALL_DEVSWARM_SUPERVISOR |
Force the DevSwarm supervisor context on/off, or auto-detect. |
devswarm.requiredGates |
done,merged,tests_passed |
ANTIHALL_DEVSWARM_REQUIRED_GATES |
Merge gates required for DevSwarm tasks. |
devswarm.inboxCmd |
— | ANTIHALL_DEVSWARM_INBOX_CMD |
Consumer-configured command to read pending mesh messages (no built-in default). |
devswarm.heldPartitions adv |
— | ANTIHALL_DEVSWARM_HELD_PARTITIONS |
Owner-held mesh partition ids (csv). Exempt from the per-turn "ORPHANED MESH" warning and from reap-orphans; still shown via diagnose/healthcheck as held by owner. |
devswarm.childGateStrict adv |
true |
ANTIHALL_DEVSWARM_CHILD_GATE_STRICT |
Strict child-gate enforcement. |
devswarm.stableLauncher adv |
true |
ANTIHALL_DEVSWARM_STABLE_LAUNCHER |
Point injected DevSwarm directive text (wake cron, Monitor re-arm, comms override, drain nudge) at the version-independent launcher under ~/.anti-hall/bin/; false reverts to the version-pinned plugin-cache path. |
devswarm.parentGateCap adv |
3 [2..5] |
ANTIHALL_DEVSWARM_PARENT_GATE_CAP |
Caps the parent-gate wait/child count, clamped to [2,5]. |
devswarm.parentGateNeglectMinUnread adv |
0 [0..] |
ANTIHALL_DEVSWARM_PARENT_GATE_NEGLECT_MIN_UNREAD |
Minimum real-unread count a genuinely NOT-busy child needs before the parent Stop gate hard-blocks on it; 0 = any real unread blocks. A BUSY child (fresh real-work transcript) gets an advisory line instead, until its oldest unread passes parentGateBusyMaxAgeMin. A child waiting on its own question or plan approval always blocks. |
devswarm.parentGateBusyFreshMin adv |
5 [1..] |
ANTIHALL_DEVSWARM_PARENT_GATE_BUSY_FRESH_MIN |
A child counts as busy only if its transcript was written within this many minutes and its latest turn is real work (not a mailbox ping, not waiting). A live process or a fresh heartbeat alone never counts. An unresolved tool call on a transcript quiet longer than this counts as waiting (permission prompt or hung tool). |
devswarm.parentGateBusyMaxAgeMin adv |
60 [1..] |
ANTIHALL_DEVSWARM_PARENT_GATE_BUSY_MAX_AGE_MIN |
Even a busy child blocks once its oldest unread message is older than this many minutes ("busy but hasn't read mail in Xm"). An unknown message age never qualifies for the busy advisory. |
devswarm.parentGateNeglectGraceMin adv |
1 [1..] |
ANTIHALL_DEVSWARM_PARENT_GATE_NEGLECT_GRACE_MIN |
Grace window (minutes) for a plain unread backlog on the parent Stop gate: an unread message younger than this never counts as neglect by itself, independent of whether the child is separately provably busy. Never applies to a store-only row, and never suppresses an unanswered child question or an escalation. |
devswarm.activeFloorPct adv |
50 [0..100] |
ANTIHALL_DEVSWARM_ACTIVE_FLOOR_PCT |
Min percent of active workspaces kept in the archived cache (0 disables the floor). |
devswarm.archivedCacheMaxAgeMs adv |
— [0..] | ANTIHALL_DEVSWARM_ARCHIVED_CACHE_MAX_AGE_MS |
computed: no fixed default — 2x the reconcile sweep’s own resolved cooldown (itself env/default-derived), not a literal constant. |
devswarm.archivedGraceMs adv |
600000 [0..] |
ANTIHALL_DEVSWARM_ARCHIVED_GRACE_MS |
Grace period (ms) before a workspace is considered archived. |
devswarm.cooldownSec adv |
600 [0..] |
ANTIHALL_DEVSWARM_COOLDOWN_SEC |
Supervisor cooldown (sec) between recovery actions. |
devswarm.idleSec adv |
900 [60..] |
ANTIHALL_DEVSWARM_IDLE_SEC |
Supervisor idle threshold (sec). |
devswarm.dormantMs adv |
1800000 (>0) |
ANTIHALL_DEVSWARM_DORMANT_MS |
Dormant-workspace threshold (ms). |
devswarm.drainTtlMs adv |
600000 [0..] |
ANTIHALL_DEVSWARM_DRAIN_TTL_MS |
TTL (ms) for the drain marker. |
devswarm.graceSec adv |
5 [1..60] |
ANTIHALL_DEVSWARM_GRACE_SEC |
Grace window (sec) before recovery in devswarm-recover. |
devswarm.maxRecoveries adv |
3 [1..20] |
ANTIHALL_DEVSWARM_MAX_RECOVERIES |
Max auto-recovery attempts. |
devswarm.intervalSec adv |
90 [60..120] |
ANTIHALL_DEVSWARM_INTERVAL |
Sweep interval (sec) used at supervisor install time, clamped [60,120]. |
devswarm.migrateMarkRead adv |
false |
ANTIHALL_DEVSWARM_MIGRATE_MARK_READ |
Mark migrated messages as read during state migration. |
devswarm.monitorTimeoutSec adv |
30 [0..] |
ANTIHALL_DEVSWARM_MONITOR_TIMEOUT_SEC |
Bounded cadence (sec) for monitor timeout in devswarm-ingest. |
devswarm.monitorNoOkFailMin adv |
10 (>0) |
ANTIHALL_DEVSWARM_MONITOR_NO_OK_FAIL_MIN |
Minutes without a successful ingest monitor poll before health reads FAILING (a fresh daemon reads "starting up" inside it). |
devswarm.nudgeCooldownSec adv |
120 [0..] |
ANTIHALL_DEVSWARM_NUDGE_COOLDOWN_SEC |
Cooldown (sec) between supervisor nudges. |
devswarm.nudgeMaxAttempts adv |
2 [1..20] |
ANTIHALL_DEVSWARM_NUDGE_MAX_ATTEMPTS |
Max nudge attempts before escalation. |
devswarm.nudgeWindowSec adv |
180 [1..] |
ANTIHALL_DEVSWARM_NUDGE_WINDOW_SEC |
Window (sec) for counting nudge attempts. |
devswarm.postSpawnGraceSec adv |
120 [0..1800] |
ANTIHALL_DEVSWARM_POST_SPAWN_GRACE_SEC |
Grace period (sec) right after spawning a child workspace, clamped [0,1800]. |
devswarm.reapedRetentionDays adv |
30 (>0) |
ANTIHALL_DEVSWARM_REAPED_RETENTION_DAYS |
Retention window (days) for reaped-workspace logs. |
devswarm.receiptWindowMs adv |
300000 [0..] |
ANTIHALL_DEVSWARM_RECEIPT_WINDOW_MS |
Window (ms) for parent-reply receipt tracking. |
devswarm.archiveRequestRenagHours adv |
24 [1..] |
ANTIHALL_DEVSWARM_ARCHIVE_REQUEST_RENAG_HOURS |
Hours a pending archive-request suppresses the CHILD NOT DRAINING nag and the ARCHIVE-READY re-nudge for that child before re-nagging anyway. |
devswarm.reconcileSweep adv |
auto (auto/off) |
ANTIHALL_DEVSWARM_RECONCILE_SWEEP |
Enable/disable the periodic reconcile sweep in the supervisor. |
devswarm.reconcileSweepSec adv |
900 [300..] |
ANTIHALL_DEVSWARM_RECONCILE_SWEEP_SEC |
Interval (sec) for the reconcile sweep, floor 300s. |
devswarm.sweepTailMode adv |
node (node/engine) |
ANTIHALL_DEVSWARM_SWEEP_TAIL_MODE |
Who decides the DevSwarm sweep tail (archived registry rows, twin descriptors): node = the scheduled Node functions (default); engine = the engine decides each step only after Node's own function, run on a scratch mirror, agrees with it. |
devswarm.rowStaleMs adv |
86400000 [0..] |
ANTIHALL_DEVSWARM_ROW_STALE_MS |
Staleness threshold (ms) for workspace row selection. |
devswarm.sendReceiptRetentionDays adv |
7 (>0) |
ANTIHALL_DEVSWARM_SEND_RECEIPT_RETENTION_DAYS |
Retention window (days) for send-receipt records. |
devswarm.summaryRetentionDays adv |
30 [0..] |
ANTIHALL_DEVSWARM_SUMMARY_RETENTION_DAYS |
Retention window (days) for summary records. |
devswarm.wakeCron adv |
7,37 * * * * |
ANTIHALL_DEVSWARM_WAKE_CRON |
Wake-poll cron schedule override (treated as untrusted input). |
devswarm.rearmOnTickOnly adv |
true |
ANTIHALL_DEVSWARM_REARM_ON_TICK_ONLY |
Token savings (0.117.0): re-arm a lapsed Monitor wake-watch only from the cron tick's watcherArmed:false check, never inline on the Monitor's own expiry event. Metric: ~/.anti-hall/devswarm/rearm-cues.jsonl. |
devswarm.wakeWatchPollMs adv |
2000 [250..60000] |
ANTIHALL_DEVSWARM_WAKE_WATCH_POLL_MS |
Poll interval (ms) for the wake-watch loop, clamped [250,60000]. |
devswarm.wakeWatchIdleSkip adv |
true |
ANTIHALL_DEVSWARM_WAKE_WATCH_IDLE_SKIP |
Token savings (#39): a Primary with 0 live child workspaces (live = not archived, not held, not ignored) skips arming/re-arming the wake-watch Monitor — nothing will ever message it. inbox tick reports watcherArmed idle-skip (not false); devswarm-wake-watch.js, if started anyway, prints one line and exits 0. Cron fallback unaffected. Metric: ~/.anti-hall/devswarm/rearm-cues.jsonl (trigger idle-skip), surfaced by doctor. Sibling behavior (no separate setting): while limitConserve is active, inbox tick reports watcherArmed limit-skip instead — re-arming a wake-watch is deferred non-urgent work, same as everything else LIMIT CONSERVATION routes away from the main agent. Metric: same ledger, trigger limit-skip, surfaced by doctor. |
devswarm.dispatchTierText adv |
true |
ANTIHALL_DEVSWARM_DISPATCH_TIER_TEXT |
The DevSwarm PRIMARY dispatch-tier text ("the workspace is your top fan-out tier") injected by task-tracker.js, verify-first.js and verify-first-orch.js, all through one gate (hooks/lib/primary-tier.js). Off removes it everywhere. It is also never injected when the repo's CLAUDE.md/AGENTS.md forbids workspaces for real work (the noWorkspaceRepo check: jev.dispatchTierNoWorkspaceRepos, jev.dispatchTierDetectNoWorkspaces). A child workspace never gets it. |
devswarm.inlineWorkNudge adv |
true |
ANTIHALL_DEVSWARM_INLINE_WORK_NUDGE |
Advisory, Primary only: once per session, when actionable tasks are pending, the Primary has PROVEN zero live child workspaces (hasLiveChild() === false; unknown liveness stays silent), and its main thread has made more than inlineWorkNudgeThreshold Edit/Write/MultiEdit/NotebookEdit calls, one note says workspace-scale work belongs in a child workspace. Rides the edit-guard.js PreToolUse hook (no extra hook entry), so it is inactive while safety.editGuard is off or edit-guard is skipped, and it is emitted only on an allowed call. Never blocks; suppressed in a no-workspace repo and in a child workspace. |
devswarm.inlineWorkNudgeThreshold adv |
5 [>=1] |
ANTIHALL_DEVSWARM_INLINE_WORK_NUDGE_THRESHOLD |
Main-thread edit calls a Primary may make before the inline-work nudge fires (it fires on the next call). |
devswarm.tickRosterEvery adv |
0 (off) [>=0] |
ANTIHALL_DEVSWARM_TICK_ROSTER_EVERY |
Every Nth inbox tick --quiet of a Primary appends the compact roster table (the same renderer as plain roster, no new columns) AFTER the unchanged first line, only when unread is 0, the tick is known, and a live non-self child is proven (a live roster row and hasLiveChild, which fails open to "has a child" on doubt). --quiet is the cron-prompt one-line rendering, so the roster belongs to it alone: the JSON form (no --quiet, or --json) never changes and --child ticks never carry it. The tick count lives as seq in the wake-tick marker, written only while this is on. |
devswarm.childGateRetentionDays adv |
14 (>0) |
ANTIHALL_DEVSWARM_CHILD_GATE_RETENTION_DAYS |
Days a per-session child-gate state file is kept before the housekeeping/doctor sweep removes it. |
devswarm.housekeepingSweep adv |
auto (auto/off) |
ANTIHALL_DEVSWARM_HOUSEKEEPING_SWEEP |
Supervisor disk-hygiene sweep (reaped logs, child-gate state); only "off" disables it. |
devswarm.housekeepingSweepSec adv |
3600 [300..] |
ANTIHALL_DEVSWARM_HOUSEKEEPING_SWEEP_SEC |
Seconds between housekeeping sweeps, floor 300. |
devswarm.supervisorLogRotateBytes adv |
10485760 (>0) |
ANTIHALL_DEVSWARM_SUPERVISOR_LOG_ROTATE_BYTES |
Size at which the supervisor rotates its own log. |
devswarm.inboxGraceSec adv |
120 [0..] |
ANTIHALL_DEVSWARM_INBOX_GRACE_SEC |
Grace window (sec) before a child's fresh unread is flagged, unless it heartbeats first; 0 = no grace. |
devswarm.supervisorSweepBudgetMs adv |
20000 [0..] |
ANTIHALL_SUPERVISOR_SWEEP_BUDGET_MS |
Time budget (ms) for one supervisor sweep pass. |
devswarm.supervisorBlockerLabelReaskSec adv |
21600 [60..] |
ANTIHALL_DEVSWARM_SUPERVISOR_BLOCKER_LABEL_REASK_SEC |
Seconds a supervisorBlockerLabel ask/log is suppressed while its input (childId+kind+ts) is unchanged, before a periodic re-ask fires anyway. |
devswarm.autoArchive.mode |
on (on/dry-run/off) |
ANTIHALL_DEVSWARM_AUTO_ARCHIVE_MODE |
Auto-archive finished workspaces (needs DevSwarm ≥ 2.5.3). |
devswarm.autoArchive.idleMin adv |
30 [5..] |
ANTIHALL_DEVSWARM_AUTO_ARCHIVE_IDLE_MIN |
Minutes idle before a finished workspace is eligible for auto-archive. |
devswarm.autoArchive.maxPerSweep adv |
3 [1..20] |
ANTIHALL_DEVSWARM_AUTO_ARCHIVE_MAX_PER_SWEEP |
Max workspaces auto-archived in one sweep. |
devswarm.autoArchive.ignorePings adv |
true |
ANTIHALL_DEVSWARM_AUTO_ARCHIVE_IGNORE_PINGS |
Idle timer ignores a finished workspace's own mailbox-wake/heartbeat/status turns; real work (an AI turn, a tool call, a new message, a commit) still resets it. |
devswarm.retention.days adv |
30 [0..] |
ANTIHALL_DEVSWARM_RETENTION_DAYS |
Days of message bodies kept before archive+prune; 0 = retention off. |
devswarm.retention.maxStoreMB adv |
100 [0..] |
ANTIHALL_DEVSWARM_RETENTION_MAX_STORE_MB |
Store size limit (MB): above it, oldest bodies are pruned regardless of age; 0 = no limit. |
devswarm.retention.keepPerPartition adv |
200 [0..] |
ANTIHALL_DEVSWARM_RETENTION_KEEP_PER_PARTITION |
Newest messages per partition that are never pruned (age or size). |
devswarm.retention.archive adv |
true |
ANTIHALL_DEVSWARM_RETENTION_ARCHIVE |
Write pruned bodies to the gzip archive first (restorable via devswarm.js retention restore). |
devswarm.retention.archiveMaxMB adv |
0 [0..] |
ANTIHALL_DEVSWARM_RETENTION_ARCHIVE_MAX_MB |
Archive size cap (MB); 0 (default) = never evict; above a set cap the oldest archive months are dropped. doctor warns past 500 MB. |
devswarm.parentGate |
true |
— | devswarm-parent-gate (Stop): make a Primary attend to a child with unread mail or a stale verdict before stopping. |
devswarm.childGate |
true |
— | devswarm-child-gate (Stop): make a child workspace heartbeat/report to its parent before going idle. |
devswarm.parentInbox |
true |
— | devswarm-parent-inbox (UserPromptSubmit): inject the workspace roster and unread child mail into a Primary. |
devswarm.childTurn |
true |
— | devswarm-child-turn (UserPromptSubmit): inject a child workspace's pending mail each turn. |
devswarm.childRole |
true |
— | devswarm-child-role (SessionStart): inject the mesh-only messaging directive into Primary and child sessions. |
devswarm.childDrain |
true |
— | devswarm-child-drain (PostToolUse Bash): re-surface a child's unread mail mid-task (throttled). |
devswarm.parentReplyTracker |
true |
— | devswarm-parent-reply-tracker (PostToolUse Bash): record the Primary's direct replies so the parent gate can tell read from answered. |
devswarm.commsGuard |
true |
— | devswarm-comms-guard (PreToolUse SendMessage): block SendMessage to a DevSwarm workspace (mesh messaging only). |
devswarm.inboxReadGuard |
true |
— | inbox-read-guard (PreToolUse Read): block raw Read-tool reads of the DevSwarm inbox/store (use the wrapper). |
devswarm.wakeWatch |
true |
— | devswarm-wake-watch monitor: wake an idle session the moment new mesh mail lands (the cron fallback stays). |
devswarm.appSync |
true |
ANTIHALL_DEVSWARM_APP_SYNC |
Supervisor app-DB sync: apply the DevSwarm app database (archive state, names, drift) every tick. |
devswarm.screenshotSync |
true |
— | devswarm.js sync-ui: reconcile a transcribed sidebar screenshot against the app DB. |
devswarm.spawnFromOrigin |
true |
— | devswarm.js spawn: fetch origin first and fast-forward the local default branch so a child never starts from stale tooling; refuses when it is behind and cannot be updated (unless --from-local). |
devswarm.sendMultiRecipient |
true |
ANTIHALL_DEVSWARM_SEND_MULTI_RECIPIENT |
devswarm.js send --to <id1>,<id2> (or a repeated --to) sends one body to each deduped recipient, attempts every recipient even after a failure, reports per-recipient ok/seq/bytes, and exits non-zero if any failed. false restores the old parsing (last --to wins). |
devswarm.spawnStrictFlagValues |
true |
ANTIHALL_DEVSWARM_SPAWN_STRICT_FLAG_VALUES |
devswarm.js spawn: refuse when -s/--source, -a/--agent, -t/--title or -p/--prompt has no value or is given what looks like the next option (spawn b -t -p "brief" used to make -p the title), naming the flag. -p accepts a brief that starts with a markdown bullet. |
devswarm.spawnFetchTtlSec adv |
300 |
ANTIHALL_DEVSWARM_SPAWN_FETCH_TTL_SEC |
devswarm.js spawn: skip the origin fetch when the remote-tracking ref was already updated within this many seconds (0 = always fetch). |
devswarm.spawnCreateTimeoutMs adv |
180000 |
ANTIHALL_DEVSWARM_SPAWN_CREATE_TIMEOUT_MS |
Timeout (ms) for the hivecontrol workspace create call spawn makes; on timeout only our own child process is killed. |
devswarm.planTracking |
true |
ANTIHALL_DEVSWARM_PLAN_TRACKING |
Step-plan tracking: spawn turns a numbered list in -p into the child's plan; the table and roster show "3/7 done · doing #4 · 42m · progress 18m ago". Off: no plan is written or shown. |
devswarm.planRequired |
false |
ANTIHALL_DEVSWARM_PLAN_REQUIRED |
Ask every child without a step plan to write one (plan set) each turn. Never refuses a spawn. |
devswarm.stepStallMin adv |
30 [5..] |
ANTIHALL_DEVSWARM_STEP_STALL_MIN |
Minutes a busy planned child may go without step progress (or since its last correction) before one stall straying warning; also the "correction followed" window. |
devswarm.strayWarnMax adv |
2 [0..10] |
ANTIHALL_DEVSWARM_STRAY_WARN_MAX |
Most straying warnings per signal per plan step. 0 turns straying warnings off. |
devswarm.burnTokensWarn adv |
2000000 [0..] |
ANTIHALL_DEVSWARM_BURN_TOKENS_WARN |
Token-burn warning: weighted tokens a planned child may spend since its last step progress before one burn warning. 2M ≈ 100+ calls at a 150k cached context with the 10% cache-read weight. 0 = off. |
devswarm.burnCacheReadPct adv |
10 [0..100] |
ANTIHALL_DEVSWARM_BURN_CACHE_READ_PCT |
Percent weight of cache-read tokens in the burn figure; 10 mirrors cache-read pricing (a tenth of the base input rate). |
devswarm.respawnGraceMin adv |
20 [0..] |
ANTIHALL_DEVSWARM_RESPAWN_GRACE_MIN |
Minutes after a correct warning before the Primary may run devswarm.js respawn <id>. Respawn is never automatic and refuses without a warning. |
devswarm.respawnWipWaitSec adv |
120 [0..] |
ANTIHALL_DEVSWARM_RESPAWN_WIP_WAIT_SEC |
Seconds respawn waits for the child to commit and push; anything left is parked on a new pushed park/<branch>-<ts> branch. |
devswarm.archivedChildStop |
true |
ANTIHALL_DEVSWARM_ARCHIVED_CHILD_STOP |
An archived child workspace can never re-register its descriptor and is told once to save a handover and stop, instead of being nagged to heartbeat forever. Its mailbox wake-watch also stays silent (no wake line, resumes if restored) and inbox tick --child reports watcherArmed archived-skip instead of false (no re-arm). A flat .anti-hall/handovers/*.md written at/after 24 h before the archive counts as the handover; the archived turn/Stop text also tells the child to delete its own inbox tick cron. false reverts to pre-fix behaviour. |
devswarm.cronMissingWarnMin adv |
60 [>0] |
ANTIHALL_DEVSWARM_CRON_MISSING_WARN_MIN |
Minutes without a fresh inbox-tick marker (cron not running — e.g. after a DevSwarm crash/session restore/Claude restart) before the Stop gate warns once, capped, to CronList/CronCreate the wake cron again. |
devswarm.maintainerNotice.post safety |
false |
ANTIHALL_DEVSWARM_MAINTAINER_NOTICE_POST |
Allow devswarm.js notice --post from THIS checkout; also requires this checkout's own plugin.json to name "anti-hall" (a mistake guard, not authentication). Only the owner changes it (here or via env); agents cannot. |
devswarm.maintainerNotice.show |
true |
— | Show unseen maintainer notices to a Primary. Off suppresses the surface only — posting/listing via the CLI is unaffected. |
devswarm.startupSampling |
true |
ANTIHALL_DEVSWARM_STARTUP_SAMPLING |
Supervisor reconcile sweep: opportunistically probe hivecontrol workspace info <id> (read-only, bounded, 3s timeout) for stale/not-draining rows and log a non-null startup field or a terminalId change — pure data capture toward designing paused-workspace detection. |
devswarm.pausedProbeMax adv |
8 [1..20] |
ANTIHALL_DEVSWARM_PAUSED_PROBE_MAX |
Max hivecontrol workspace info probes per supervisor sweep tick for the startup-state sampler above. |
statusline.base |
— | ANTIHALL_STATUSLINE_BASE |
Shell command run as the line-1 base in consolidated statusline mode. |
statusline.noEmail |
false |
ANTIHALL_STATUSLINE_NO_EMAIL |
Suppress the email segment in the statusline. |
codexNudge.enabled |
true |
ANTIHALL_CODEX_NUDGE |
Enable the Codex hand-off nudge hook. |
codexNudge.min adv |
3 [1..] |
ANTIHALL_CODEX_NUDGE_MIN |
Minimum substantial code-file edits before the nudge fires. |
engine.bootstrap |
true |
AH_ENGINE_BOOTSTRAP |
Download and install the sha256-pinned ah-engine binary from the GitHub Release on SessionStart (once per pinned release). Off: nothing is downloaded and the Node hooks answer everything. AH_ENGINE_BOOTSTRAP=0/1 overrides this key. |
engine.autoUpdate |
off [off, stable, dev] |
AH_ENGINE_AUTO_UPDATE |
Update the engine binary on its own, at most once a day: off (default), stable (latest release) or dev (latest dev pre-release; also syncs the plugin files of a live kit). Runs hooks/ah-update.sh --auto; the previous binary is kept (--rollback). URLs and timeouts: engine/ah-update.toml. |
defects.defaultProj |
— | ANTIHALL_DEFECT_PROJ |
Default project tag used when filing an anti-hall defect (max 64 chars). |
procwatch.enabled |
true |
ANTIHALL_PROCWATCH |
procwatch (scheduled sweep + SessionStart/UserPromptSubmit/PreToolUse advisory): look for processes a Claude session left behind (marked by the environment Claude Code sets, owner session gone, class pattern, minimum age) and for agents with no output. Never touches a live session, an unmarked process or a system process. |
procwatch.devServerMode |
report |
ANTIHALL_PROCWATCH_DEV_SERVER |
dev_server class of the process watch: dev servers and watchers an agent started. off | report (list only, the default) | kill (stop them one pid at a time after a grace period). |
procwatch.testRunnerMode |
report |
ANTIHALL_PROCWATCH_TEST_RUNNER |
test_runner class of the process watch: test runners and their children. off | report (list only, the default) | kill (stop them one pid at a time after a grace period). |
procwatch.buildDaemonMode adv |
report |
ANTIHALL_PROCWATCH_BUILD_DAEMON |
build_daemon class of the process watch: build tool daemons. off | report (list only, the default) | kill (stop them one pid at a time after a grace period). |
procwatch.mcpServerMode adv |
report |
ANTIHALL_PROCWATCH_MCP_SERVER |
mcp_server class of the process watch: MCP servers of ended sessions (the SessionEnd reaper, maintenance.sessionEndReaper, is separate). off | report (list only, the default) | kill (stop them one pid at a time after a grace period). |
procwatch.shellTaskMode adv |
report |
ANTIHALL_PROCWATCH_SHELL_TASK |
shell_task class of the process watch: background shell commands of ended sessions. off | report (list only, the default) | kill (stop them one pid at a time after a grace period). |
procwatch.otherMode adv |
report |
ANTIHALL_PROCWATCH_OTHER |
other (catch-all) class of the process watch: any other process a Claude session started and left behind, oldest first. off | report (list only, the default) | kill (stop them one pid at a time after a grace period). |
procwatch.stuckMinutes |
20 |
ANTIHALL_PROCWATCH_STUCK_MINUTES |
Minutes without output after which a background agent of this session is named in a stuck-agent advisory (UserPromptSubmit; warn only, once per cooldown). Reuses the silent-agent-nudge detection. |
resourceWatch.enabled |
true |
ANTIHALL_RESOURCE_WATCH |
resource-watch: sample the processes under live Claude sessions each sweep and warn the session (advisory only). |
resourceWatch.cpuPercent |
90 |
ANTIHALL_RESOURCE_WATCH_CPU |
Per-core CPU percent (100 = one core busy; a multi-threaded process can exceed it) every sample of the window must reach. |
resourceWatch.cpuWindowSeconds adv |
120 |
ANTIHALL_RESOURCE_WATCH_CPU_WINDOW |
Seconds the CPU reading must hold. |
resourceWatch.memoryMb |
4096 |
ANTIHALL_RESOURCE_WATCH_MEM |
Memory in MB (resident set on Linux, physical footprint on macOS) at which a process of a live session is named. |
resourceWatch.swapMb adv |
8192 |
ANTIHALL_RESOURCE_WATCH_SWAP |
System swap in use, MB, that triggers a warning; 0 = off. |
resourceWatch.pressurePercent adv |
25 |
ANTIHALL_RESOURCE_WATCH_PSI |
Linux memory pressure (PSI some avg10, percent) that triggers a warning; 0 = off. |
resourceWatch.macPressureLevel adv |
2 |
ANTIHALL_RESOURCE_WATCH_MAC_PRESSURE |
macOS memory pressure level (2 warn, 4 critical) that triggers a warning; 0 = off. |
resourceWatch.cooldownSeconds adv |
900 |
ANTIHALL_RESOURCE_WATCH_COOLDOWN |
Least seconds before the same process (or system warning) is named again. |
resourceWatch.renice |
false |
ANTIHALL_RESOURCE_WATCH_RENICE |
Opt-in: lower the priority (nice 10) of a process the watch warned about, once. Off by default; the watch never kills. |
diskWatch.enabled |
true |
ANTIHALL_DISK_WATCH |
disk-watch: warn (SessionStart/UserPromptSubmit) when the project, HOME or temp volume is below the warn floor, and before heavy commands (PreToolUse) at the critical floor; names the biggest build/cache directories as a suggestion. |
diskWatch.warnGb |
20 |
ANTIHALL_DISK_WATCH_WARN_GB |
Warn below this many GB free (0 = not used). |
diskWatch.warnPercent adv |
10 |
ANTIHALL_DISK_WATCH_WARN_PCT |
Warn below this percent free (0 = not used). |
diskWatch.criticalGb |
5 |
ANTIHALL_DISK_WATCH_CRITICAL_GB |
Critical below this many GB free (0 = not used). |
diskWatch.criticalPercent adv |
3 |
ANTIHALL_DISK_WATCH_CRITICAL_PCT |
Critical below this percent free (0 = not used). |
diskWatch.cooldownSeconds adv |
1800 |
ANTIHALL_DISK_WATCH_COOLDOWN |
Least seconds before the same level is warned about again (a worse level always is). |
diskWatch.blockAtCritical |
false |
ANTIHALL_DISK_WATCH_BLOCK |
Opt-in: at the critical level, block heavy commands (builds, clones, worktree add) instead of only warning. Off by default. |
Configuration / tuning¶
- Verify-first wording — edit
hooks/verify-first-core.js(the single source of every protocol text; then runnode tools/gen-protocol.jsto regeneratePROTOCOL.md,protocol-md.test.jsfails on drift) and theNUDGESarray inhooks/verify-first.js(the per-turn one-liners). - Hard gates / force patterns —
hooks/git-guard.jsholds the commit-trailer and force-push logic;command-guard.jsand the other always-on guards cover deploy CLIs, payment commands, and bulk deletes at command dispatch.ship-itrelies on these always-on guards for its hard safety boundaries rather than a bespoke per-project sentinel. - Task discipline — edit the respective
hooks/*.js. All hooks are fail-open: a bug in a hook must never wedge a turn.
Troubleshooting / FAQ¶
- Hooks not firing? Restart Claude Code so a fresh session re-runs SessionStart,
and ensure
nodeis onPATHfor the shell Claude Code launches hooks from (node --version). Ifnodeis missing, all hooks silently no-op. - Statusline didn't apply? It is opt-in — run the installer above. If it reports
"not found", run
/plugin installfirst, then re-run, or locate the dir via/plugin. - git-guard let a force-push through? Check the documented fail-open scope above
(aliases / interactive-editor commits with no
-m/-Fare out of scope by design;bash -c/sh -cwrappers are unwrapped and inspected, not a bypass). - Guard blocking something legitimate? Most guards fail open and have a skip hatch
(
~/.anti-hall/skip.json, per-guard, TTL'd) —git-guardmust be named explicitly. - Statusline not showing? Restart Claude Code once after installing —
statusLineis only read at startup. - Update didn't take effect? Run
/reload-pluginsafter/anti-hall:update. Restart Claude Code only if a hook or skill path still shows the old version afterwards (or to re-run SessionStart-only injections). - Upgrading from 0.107.x or earlier? Run
claude plugin update anti-hall@anti-hallonce, then restart Claude Code — the oldupdatecannot register 0.108.0 with the harness. Later updates do this themselves. - Anything else? KB.md is the doc index; file a defect with
/anti-hall:defects. - Using Codex too? Copy
AGENTS.md(repo root) into your own repo root — it is not bundled by/plugin install. Verify withcodex --ask-for-approval never "Summarize current instructions".
Test locally¶
# Full zero-dependency E2E suite (node:test, run from the repo root):
node --test # 2693 pass +2 skipped (2695 total); CI runs the same on push/PR (.github/workflows/test.yml)
# Quick smoke-checks of individual hooks:
echo '{"hook_event_name":"SessionStart"}' | node hooks/verify-first-full.js # full Iron-Law protocol + skill primer
echo '{"prompt":"x"}' | node hooks/verify-first.js # short varying nudge (varies by full stdin envelope)
echo '{"prompt":"y"}' | node hooks/verify-first.js # different envelope -> different nudge
claude --plugin-dir /path/to/anti-hall # load in a throwaway session