Heimdall makes your coding agent's work pass a test it didn't write — and proves that test can fail — before the push lands.
A Claude Code plugin. One prompt in, a gated, receipted change out. The gate is a plain git pre-push hook, so it holds in any repo and also gates Cursor CLI. Your agent's own tests are a claim; the oracle Heimdall wires is external, and it is first shown to go red on a planted defect before its green is trusted.
Install — pinned to a tag and sha256-checked; if the digest fails, nothing runs (what it writes to your machine · what leaves it):
curl -fsSL https://raw.githubusercontent.com/randomittin/heimdall/v2.4.3/install.sh -o heimdall-install.sh \
&& echo "a03c15bfdddd096f3152204c047dbf6888b34194592b0707ee82a892da147a67 heimdall-install.sh" | shasum -a 256 -c - \
&& bash heimdall-install.shRun first:
hmd demoDry by default — scaffolds a fully-specified full-stack task into ./heimdall-demo-app and prints the paste-ready next step; it executes nothing until you add --run. On a terminal, the first-ever run also plays the narrated build in which a planted credential is caught by the real bin/secret-scan gate — deny → fix → pass — the same arc every gated change goes through.
Check the proof yourself, no trust required (~1 min, network-free after the clone):
git clone https://github.com/randomittin/heimdall && cd heimdall
bin/falsify exchange-lob --assert-score 1.0 # every mutant must turn the gate RED, or exit non-zero
bin/falsify emulator-gb --assert-score 1.0Real output from this tree (2026-09-19), last lines of each run — a survived mutant would print REJECTED and fail the assert:
SCORE: 6/6 = 1.0000 (golden passing) (incl. 2 guard, gate-invoked)
ASSERT PASS: score 1.0000 >= target 1.0 (golden passed, no mutant survived)
SCORE: 3/3 = 1.0000 (golden passing)
ASSERT PASS: score 1.0000 >= target 1.0 (golden passed, no mutant survived)
bin/corpus run replays the 13 real shipped-failure cases through the same gates and appends a per-version catch-rate row to evals/corpus/CORPUS-STATUS.md — it writes to the tree, so it is not in the read-only block above. Its current score and the ❌ rows that are kept in view: evals/flagship/STATUS.md.
| Number | Reproduce it |
|---|---|
| 427 test suites | ls test/*.test.sh | wc -l |
10,127 assertions passed, 412/412 suites, 0 failed — last full sweep, at commit b43c4f4b (suites added since account for 427 on disk) |
bash test/run-all.sh (~17 min) writes .heimdall/receipts/last-sweep.json; then jq .assertions_passed .heimdall/receipts/last-sweep.json |
| 13/13 corpus cases caught (100%) | bin/corpus run (mutates CORPUS-STATUS.md); scoreboard row in evals/flagship/STATUS.md |
| 1.0 falsifiability on both flagship oracles (6/6, 3/3) | the two bin/falsify … --assert-score 1.0 commands above |
Then: hmd invite puts a teammate on your team wall in one paste · cloud bot that opens gated PRs on your repo: rr · a default module is a local proxy that reads your prompts — disclosed in full under Install.
Three paths. Each is labelled with what it actually does to your machine — pick the risk you are willing to take, not the shortest command.
| # | Path | What it runs | Honest risk |
|---|---|---|---|
| 1 | One-liner (below) | Downloads install.sh, checks its sha256, then runs it as you |
Highest. A script fetched over the network, executing with your user's privileges — it can do anything you can do. The digest check is the only thing between you and whatever those bytes are. Verify it, or take path 3. |
| 2 | npm — npx runheimdall |
The same install.sh, fetched over https and sha256-checked against a digest baked in at publish time (bin/runheimdall.js), then handed to bash |
Same as path 1. The wrapper is thin and the script still runs as you. You gain not having to hand-copy a digest. You gain no isolation. |
| 3 | Docker sandbox — Dockerfile.install |
The same install.sh, copied from your own clone — nothing fetched, no digest to trust — run inside a container |
Lowest, with real caveats. Every $HOME change lands in a layer you delete. It does not isolate a repo you mount, and it is not a macOS sandbox. Caveats below. |
Whichever you pick, this is what the installer writes outside the repo. Measured from a real run against a throwaway $HOME, not asserted:
| Written | What it is |
|---|---|
~/.heimdall/ |
A full git clone of this repo — the installed checkout hmd runs from |
~/.local/bin/hmd, ~/.local/bin/heimdall |
Symlinks into ~/.heimdall/bin/heimdall |
~/.claude/settings.json |
Adds statusLine + subagentStatusLine, and registers the plugin under enabledPlugins / extraKnownMarketplaces. Honors $CLAUDE_CONFIG_DIR, and never clobbers a statusLine you set yourself |
~/.zshrc / ~/.bashrc / ~/.profile |
One appended export PATH=… line for ~/.local/bin — only when it is not already on PATH |
~/Library/LaunchAgents/com.heimdall.dream.plist |
macOS only — a nightly 03:00 LaunchAgent (com.heimdall.dream) that runs the overnight sweep with no session open and survives logout and reboot. Opt out with HEIMDALL_NO_DREAM_SCHEDULE=1 |
~/.heimdall/team.json |
An auto-minted solo team secret, written by the post-install health check on every run (heimdall-doctor-install, deliberately pinned to $HOME) whether or not you supplied a team invite. A bearer capability — treat it like a credential |
One thing that table leaves out because it does not fit "outside the repo": pasting a team invite (HEIMDALL_TEAM_SECRET='<secret>' curl -fsSL … | bash) writes a SECOND, real team.json inside whatever repo your shell was in when you ran the installer — <repo>/.heimdall/team.json, resolved from that shell's git toplevel at install time, never ~/.heimdall (ensure_team_secret, install.sh:520-537). Measured the same way as the table above: a throwaway git-toplevel cwd distinct from $HOME got the file; the throwaway $HOME did not.
No sudo. Idempotent — re-run to upgrade. hmd uninstall reverses all of it.
Network posture is default-ON. Team presence and the cloud features reach the control plane as soon as you use them: a signed heartbeat carrying your handle, verdict, and current filename — scoped to your team, never your code or file contents. That is a feature, and it is on until you switch it off. hmd presence sever gives zero egress. Field-by-field contract: DATA.md. The precisely scoped claims are under Your code stays yours.
hmd's default module set includes a proxy that can read your prompts. hmd wrap claude routes generation traffic through it; not every hmd wrap <tool> does — see below. Headroom is default_included: true in modules/headroom/manifest.json: a local context-compression proxy which, once traffic is pointed at it, sits between your coding tool and the model provider, reads the prompts and context on their way out, and rewrites them to be smaller. hmd modules add headroom installs the package, then measures each wire the manifest declares and reports what it found: the wrap chain measures ROUTED, meaning bin/heimdall-wrap offers the proxy hop to every tool it launches — not that every tool's traffic actually flows through it. Only claude reads the ANTHROPIC_BASE_URL this repo sets, so hmd wrap claude is the one launch that truly carries GENERATION traffic through the proxy; hmd wrap codex/hmd wrap gemini set the same variable on a CLI that never reads it, so their generation traffic is NOT routed despite the wire measuring ROUTED, and hmd wrap cursor/hmd wrap aider are unconfirmed either way. Bare hmd does not route. The storage-codec wire still measures RECORDED, not routed. JUDGMENT never traverses it — every verdict-producing call is scrubbed back to the real provider, which test/gate-judgment-uncompressed.test.sh goes red on. It runs as a process you own and can inspect, and it introduces no Heimdall-operated destination — that traffic goes to the same provider it went to before. Five things to know before you install:
- Nothing installs it for you.
install.shhas no module code path at all, and the background updater refuses to acquire a consent-required class unattended — it names the module and hands you the command. Until you runhmd modules add headroom,hmd modules status headroomreportsNOT ATTEMPTED. - That command is a remote code install, and no digest is verified. It runs
uv tool install --python 3.13 "headroom-ai[all]==<pin>"against PyPI. hmd hashes nothing on that path and does not claim to: the lifecycle step is namedinstall + provenance, notdigest-verify, and the receipt recordsverified: falsealongside the pin it did not check. It also pulls an ML stack — Rust wheels, an ONNX runtime, HuggingFace tokenizers — so this is the one place hmd stops being near-stdlib. - The consent question is waived; the disclosure is not.
consent_waivedsits on that one module's manifest.modules/_classes/traffic-proxy.jsonstill readsconsent_required: true, so every other traffic-proxy module hmd ships still asks. The consent text still prints, both declared class contracts still run their invariants, and the receipt recordsgranted_via: manifest-waiver. This is a deliberate maintainer decision: disclosed, not asked. - Installing it is not wiring it, and hmd measures the difference out loud. The manifest declares two wires and hmd applies neither.
[6/7] wireprints each one asRECORDED, not routednext to the measurement that produced it —bin/heimdall-wrapholds no reference to the module, and the memory-codec seam reportsbackend=plain— andhmd modules status headroomreads back that same record. A declared wire whose kind the code has no handler for is refused at validate rather than quietly recorded, so a module cannot install while claiming a capability hmd does not deliver (test/wire-kind-dispatch.test.sh). - Gates read raw, and signed traffic steps around it. Route generation through a proxy — this one, or your employer's — and judgment still may not follow it. Verdict-producing commands run through
hmd_gate_exec, and control-plane, enrollment and presence traffic throughhmd_signed_exec(bin/lib/hmd-gate-endpoint.sh); both dropANTHROPIC_BASE_URL, the proxy pairs and the wholeHEADROOM_*namespace before pinning the endpoint to the real provider.test/gate-judgment-uncompressed.test.shgoes red the moment a gate request reaches the proxy.
hmd modules remove headroom returns the tree byte-identically. Threat model, the full reachability table and every way to decline it: SECURITY.md. Mechanics and honest limits: Modules.
curl -fsSL https://raw.githubusercontent.com/randomittin/heimdall/v2.4.3/install.sh -o heimdall-install.sh \
&& echo "a03c15bfdddd096f3152204c047dbf6888b34194592b0707ee82a892da147a67 heimdall-install.sh" | shasum -a 256 -c - \
&& bash heimdall-install.shPinned to the v2.4.3 tag and checked against the sha256 of that tag's install.sh. The &&
chain is load-bearing: if the bytes do not match the digest, shasum -c prints FAILED and
nothing runs — so a tag moved under you, a CDN cache poisoning, or a truncated download stops
the install instead of executing. On a Linux box without shasum, sha256sum -c - takes the same
digest. Re-derive it yourself any time:
curl -fsSL https://raw.githubusercontent.com/randomittin/heimdall/v2.4.3/install.sh | shasum -a 256
# a03c15bfdddd096f3152204c047dbf6888b34194592b0707ee82a892da147a67No sudo. Idempotent — re-run to upgrade. Reversible:
hmd uninstall # removes everything; nothing else was touchedPrefer to inspect first? Same download, same check — just read it before you run it:
curl -fsSL https://raw.githubusercontent.com/randomittin/heimdall/v2.4.3/install.sh -o heimdall-install.sh
echo "a03c15bfdddd096f3152204c047dbf6888b34194592b0707ee82a892da147a67 heimdall-install.sh" | shasum -a 256 -c -
less heimdall-install.sh # function-wrapped, no eval, no base64 — what you read is what runs
bash heimdall-install.shSignature (stronger than the digest). A digest you copy from this README only proves the bytes
match what this README says; a signature proves they came from the maintainer's key. Every release
signs install.sh with minisign and publishes install.sh.minisig as a release asset:
curl -fsSL -O https://github.com/randomittin/heimdall/releases/download/v2.4.3/install.sh.minisig
minisign -Vp release/heimdall-signing.pub -m heimdall-install.sh -x install.sh.minisigThe public key ships in this repo at release/heimdall-signing.pub, so that command assumes a
clone. SIGNING.md has the full model — including the bundled pure-python verifier
for machines with no minisign binary, and the fail-closed behavior of the auto-updater.
Prerequisites: Claude Code 1.0+ · Git · jq (brew install jq) — install.sh itself completes without jq (every call site there guards with command -v jq), but most hmd subcommands hard-require it afterward, so install it up front
hmd is itself a Claude Code plugin, so Claude Code is what installs and runs it. If Cursor CLI's agent (cursor-agent) is also on PATH, hmd init gates that host too — see Also gates Cursor CLI.
npx runheimdall — same pinned tag, same sha256 check, zero clone required. It fetches the pinned install.sh, verifies it against the digest baked in at publish time, and aborts before executing anything if the bytes disagree. Convenience, not containment: what finally runs is the same script, with the same privileges as path 1.
For a first look that does not touch your machine. Build from a clone, so the install.sh you read is byte-for-byte the one that runs — nothing is fetched, so there is no digest for you to trust:
git clone https://github.com/randomittin/heimdall && cd heimdall
less install.sh # what you read is what runs
docker build -f Dockerfile.install -t heimdall-sandbox .
docker run --rm -it heimdall-sandbox # hmd is already on PATH--rm discards every $HOME mutation in the table above the moment the container exits. The image also drops the auto-minted team.json during the build, so containers never share one team secret.
What the container does not isolate — a sandbox you misunderstand is worse than no sandbox:
- A mounted repo is not isolated.
-v "$PWD:/work"is a hole you punched on purpose: anythinghmdwrites under/worklands on your real disk. Mount:roif you only wanthmdto read your code. - It is not a macOS sandbox. The image is Linux, so launchd and the keychain do not exist inside it and the nightly LaunchAgent step is skipped as
unsupported. That is the container being a different OS — not a boundary defending your account.launchctland the keychain are account-scoped: a fake$HOMErelocates only the plist file, whilelaunchctl loadstill registers the job in your real per-user session. A sandboxed test in this repo learned that the hard way, by rewriting the developer's live LaunchAgent. The switch isHEIMDALL_NO_DREAM_SCHEDULE=1— never$HOME. - The network is open. The build clones from GitHub, and presence is on by default.
docker run --network nonegives the container zero egress;hmd presence severdoes the same at the application level.
Gates run 100% locally. Your code never leaves your machine.
Team presence is a feature you can see and switch off: it sends {handle, verdict, current filename} — never code, never file contents — to your team's endpoint. hmd presence off makes you invisible; hmd presence on --no-files hides filenames.
Telemetry is specified, minimal, and yours to kill: DATA.md documents every field. hmd telemetry off. hmd telemetry purge deletes the local spool — nothing is transmitted in this release.
Auto-update checks GitHub Releases for new signed versions. HEIMDALL_NO_AUTOUPDATE=1 (or ~/.heimdall/no-autoupdate) disables it.
rr is the one thing that sends on purpose, and only when you run it: your BYO Claude credential (write-only), your GitHub App installation id, and the literal task text you typed — because that text is the job. It never uploads your working tree; the worker clones your repo from GitHub.
Full data contract and every field collected: DATA.md.
hmd demo --runScaffolds a real full-stack task, builds it, ends with a summary card and a follow-up prompt. Safe to run sight-unseen — hmd demo (without --run) prints the plan and does nothing.
- Catches the silent failures — ordering races, whole-sequence invariants, missing subsystems that pass a naive green suite.
- Falsifiable gates — every gate is proven able to go red before it is trusted green. The corpus of real failure cases replays on every change; a regression that once shipped can never ship twice.
- Proof of correctness, not just generation — the delta Heimdall sells is the receipt that proves the proof can fail. Generalizes: 0.50 median reuse across 8 cold repos.
- Full audit trail —
hmd reportproduces a machine-readable telemetry report of every gate, mutation score, and corpus catch-rate from the last run.
rr is the same gate, run in the cloud: a bot that fixes your GitHub issues and opens a PR whose fix has already passed a check the agent never wrote. You review, you merge. The bot opens it on a heimdall/* branch as a scoped GitHub App — never as you, never on main, and it never self-merges. A human always gates the merge.
Once you've installed the Heimdall Maintainer GitHub App on your repo and run claude setup-token, it's two commands:
rr connect # registers your App install + captures your Claude cred
rr "fix the flaky test in payments and open a PR"What happens: rr signs your task with your own Ed25519 key and enqueues it. A gated worker clones your repo with your team's Claude subscription and your GitHub App installation, runs the issue-resolution loop until the fix passes the gates, and opens a heimdall/* PR on your repo. You review it. You merge it.
Nothing to paste — no token, no URL. The public control plane is baked in and enrollment is automatic: your first signed call registers this device on first use. Just rr connect and go. (Running your own deployment, or need to re-gate enrollment behind a bootstrap token? That's an operator concern — see OPERATORS.md.)
- Tenant isolation is a falsifiable oracle, not a promise. Every cross-tenant attack — IDOR by repo slug, cred read across teams, queue drain, installation-id swap, signed-request replay — has a named invariant and a red-line mutant test. Drop any gate and
test/heimdall-cp-authz-gate.test.shgoes red; the keystone suite passes only when every mutant is caught. Full invariant + attack matrix:docs/specs/2026-07-03-rr-isolation-invariants.md. - BYOC — no shared keys. You pay your own Claude tokens; your credential lands in your own per-team Secret Manager secret and is injected env-only into your job — never logged, never echoed, never readable by another tenant.
- Least-privilege bot. The App holds exactly Contents + Issues + Pull requests — no Administration, no Actions/Workflows, no merge capability. It can open a PR; it cannot touch branch protection or push to
main. - Honest bring-up. This loop was hardened over a live multi-tenant bring-up that shook out a run of production-only failures — Google's GFE rejecting GET-with-a-body, cold-start identity drift, jobs starving under scale-to-zero — each now documented as fixed in
deploy/cloud-run/README.mdand the runbook.
Under the hood the bot runs the local engine installed above: every plan wires an external, falsifiable oracle so the implementation can never grade its own homework, and the merge stays blocked until that oracle is green — after it has been proven able to go red.
| Capability | Command | Status |
|---|---|---|
| Verification gates (secret-scan, falsify) | Automatic on commit/push (git hooks) · bin/falsify <domain> to run one directly |
Shipped |
| Demo task runner | hmd demo / hmd demo --run |
Shipped |
| Issue-resolution loop | hmd (auto-retries failures against corpus) |
Shipped |
| Telemetry report | hmd report |
Shipped |
| Design match (visual diff vs spec) | hmd designmatch |
Shipped |
| Redum / conformance checker | hmd redum · hmd check |
Shipped |
| Reuse engine (cold-repo analysis) | bin/lib/reuse_analyzer.py |
Shipped |
| Debloat scanner | heimdall-debloat --report-only |
Shipped |
| Parallel workers | hmd --team N "task" (N tmux panes, independent — no shared state) |
Shipped (no coordination layer) |
| Benchmark suite | heimdall-bench |
Shipped |
| Optional capability modules | hmd modules |
Shipped |
| Cursor CLI host (gate + statusline HUD) | hmd init (auto-detects) · details |
Shipped |
| AI backend preference (Claude Code ⇄ Cursor CLI) | heimdall-ai-select · /hmd:switch-ai |
Shipped |
Optional capability modules. The base install ships the module system and zero module payloads — modules/ holds manifests and class contracts, never vendored code. The registry is this repo, so every pin is a reviewed commit rather than a network lookup, and there is no code path that resolves "latest". Nothing self-installs.
hmd modules # list — honest when nothing is installed
hmd modules add <name> # the full ordered pipeline
hmd modules remove <name> # total removal
hmd modules update [<name>] # move to the manifest's human-set pin
hmd modules status <name> # one module in detail
hmd modules verify [<name>] # re-run class invariants (the CI entry point)
hmd modules preflight <name> # can this install happen? read-only, asks nothing
hmd modules repair <name> # retry an install, recording which stage failed
hmd modules defer <name> # not now — hmd is ready, the module follows later
hmd modules pending # what is deferred or awaiting a retry
hmd modules optout <name> # decline; no install path may re-install it
hmd modules optin <name> # undo an optoutEvery verb accepts --json; add accepts --yes. --registry and --state relocate the registry and install state, which is how the suite runs hermetically.
A module declares its class — or several — in its manifest, and a missing or unknown class is refused, not defaulted: the class decides consent and which invariants are enforced, so picking one silently would be picking a security posture on the operator's behalf. When a module declares several, the union of their invariants runs, never the first match.
| Class | Consent | Invariants enforced |
|---|---|---|
traffic-proxy |
required | gates read raw · non-interactive passthrough · signed and control-plane traffic never routed through it |
tool-adapter |
required | wrap/unwrap byte-identical · hooksPath and AGENTS.md fences preserved |
storage-codec |
not required | round-trip fidelity · plain fallback when absent · never touches judgment inputs |
rule-pack |
not required | rules ship falsifiers · attribution preserved |
The two classes that mutate something you own — the wire, and your own config files — ask. The two that are transparent by contract do not, and their invariants are what make that transparency true rather than asserted: a codec that loses a byte, or a pack that flattens attribution, is rolled back rather than merely disclosed.
[1/7] validate → [2/7] class contract → [3/7] preflight → [4/7] consent
→ [5/7] install + provenance → [6/7] wire → [7/7] class invariants (module active)
Steps 1–4 are read-only, so anything rejected at validate, class, preflight or consent mutates nothing at all. Preflight sits before consent on purpose — nobody should be asked to agree to an install that cannot happen. Step 5 is the first mutation, and from there every failure unwinds through the same removal path remove uses, so a module that fails its own class test leaves a byte-identical tree. Wiring precedes invariants deliberately: the contracts assert behaviour with the module active, so a check run against an unwired module would prove nothing.
Step 5 is called provenance, not digest-verify, and the naming is load-bearing. A digest is verified on exactly one path: a local module, whose artifact ships in this repo and is hashed here against its pin. An upstream module is fetched from a package index and hmd hashes nothing — it records the pin and asks the installer whether the payload arrived. The receipt reads verified: false in every upstream state, and nothing re-checks that pin later: hmd modules verify re-runs the class invariants and reads no digest at all. Printing digest-verify over that would assert a check that never ran.
An install that required consent records how it was granted — granted_via is one of interactive, --yes, or manifest-waiver — alongside the exact consent_text shown and its consent_text_sha256. Consent required with a non-TTY stdin is refused, not prompted and not defaulted to yes; --yes is the operator saying so on purpose.
A module in the default set reaches machines the author will never meet, so default_included: true is refused at validate time if any class it claims requires consent and the manifest ships no consent_text. A disclosure that only materialises at a prompt somebody may never be shown is not a disclosure.
Two surfaces read opt-out signals, and they do not overlap — each verb honours the signals of the surface it belongs to:
| Signal | Read by | Effect |
|---|---|---|
HEIMDALL_NO_MODULES=1 |
bin/heimdall-autoupdate |
no module is acquired automatically, at all |
~/.heimdall/modules-optout |
bin/heimdall-autoupdate |
one module name per line; # starts a comment |
hmd modules optout <name> |
bin/heimdall-modules |
persisted; repair, defer and pending all refuse to act |
HMD_MODULE_OPTOUT=a,b |
bin/heimdall-modules |
the same, for one invocation |
install.sh acquires no modules at all — the installer has no module code path, so there is no installer flag to suppress and none is offered. Automatic acquisition lives in the background updater, which is why HEIMDALL_NO_MODULES=1 bites there. The consequence, stated plainly: every one of these signals governs acquisition that happens on your behalf. HEIMDALL_NO_MODULES=1 suppresses automatic acquisition and does not block an explicit hmd modules add <name> — an operator typing the command themselves is not what an opt-out for unattended installs is trying to stop.
modules/headroom/manifest.json pins Headroom (Apache-2.0), a local context-compression proxy. Depend, don't clone — none of its source is vendored here. The manifest is the single source of truth for the pin and its artifact digest; this page deliberately does not restate the version, because a second hand-maintained copy of a pin is how pins drift.
It is default_included, and its consent question is waived on the module itself. modules/_classes/traffic-proxy.json still reads consent_required: true, so every other traffic-proxy module hmd ever ships still asks; the waiver's blast radius is this one module. Waived is the question, never the disclosure — the consent text still prints, both declared classes still run their invariants with the module active, and hmd modules remove headroom still returns the tree byte-identically.
Being in the default set is a distribution fact, not a claim that it helps, and not an unattended install:
- The background updater will not install it for you. Acquisition reads the class contract, and
traffic-proxyrequires consent — soheimdall-autoupdatenames the module, states the class, and hands youhmd modules add headroomrather than acquiring it. Until you run that command,heimdall-autoupdate statusreports itabsentwith the reason andhmd modules status headroomreportsNOT ATTEMPTED. - It is the one place hmd stops being near-stdlib. The fetch is
uv tool install --python 3.13 "headroom-ai[all]==<pin>", which pulls an ML stack — Rust wheels, an ONNX runtime, HuggingFace tokenizers. Install size and time are materially larger than hmd's own. - A failed Headroom install does not fail
hmd install. hmd works; the module reports ABSENT with the blocker and the remedy named, and rolls back through the removal path. It is never silently assumed present. - The storage-codec half does not engage via the documented install.
uv tool installlands the package in an isolated per-tool venv that hmd'spython3cannot import, so the memory codec stays on itsplainbackend on every machine that installs it the sanctioned way. The manifest records that as a measurement rather than letting a greenaddimply otherwise. Treat compression on the storage seam as a contract the seam honours if a backend ever arrives — not as a description of any running machine. - The proxy half is declared, not applied — hmd points no traffic at it.
bin/heimdall-wrapcontains no reference to the module, so installing Headroom leaves your generation traffic going exactly where it went before. Thewrap-chainwire records the chain hmd would offer; wiring it up is yours to do, deliberately, by pointing your tool at the proxy. hmd measures that wire at everyaddand printsRECORDED, not routedwhile it stays unapplied, and a wire kind with no handler behind it is refused outright rather than recorded — an install may not imply a capability the code cannot deliver (test/wire-kind-dispatch.test.sh). - Gates read raw. Generation traffic may traverse a proxy once you route it through one; judgment traffic may not follow. Every verdict-producing execution runs through
hmd_gate_exec, which unsetsANTHROPIC_BASE_URL, the HTTP/HTTPS/ALL/NO_PROXY pairs and Headroom's ownHEADROOM_*namespace before pinning the endpoint to the real provider. A judge reading compressed context emits confident false greens, which is the failure this whole project exists to prevent.
Full manifest schema, class-contract details and the lifecycle rationale: modules/README.md.
Heimdall's status bar is a full-width, four-row watchman HUD. It renders entirely shell-side (zero model, zero context cost) — bin/heimdall-statusline is one CLI-agnostic renderer shared by both hosts it wires into. Under Claude Code, hooks/statusline.sh reads the statusLine JSON Claude Code hands it on stdin. Under Cursor CLI it reads the same shape from Cursor's own statusLine plug-in point, swapping only the width signal — Cursor hands render_width_chars in that JSON instead of Claude Code's $COLUMNS, and the renderer already reads that field itself. Three surfaces, by how far they spread:
Your sigil — the identity hook. Every Heimdall identity (HAID) gets a unique, deterministic pixel watchman: same identity, same sigil, forever. It anchors the left of the line, prints big on the install card, and shares as a postable block:
python3 sentinels/hmd-sigil.py --seed $HMD_HAID --size large # share/banner render
bash hooks/hmd-banner.sh --share # postable "my watchman" cardThe seed is your HAID by default — automatic, stable, no PII in the art. Works solo on day one, before any teammate shows up.
The team watch wall — the headline, and the moat. When teammates also run hmd in the same repo, the bottom row becomes a live wall of their watchmen and what each agent is doing — gate state colored in, a teammate's cell flashing red the instant their gate denies. Nobody else can render this; it needs Heimdall's coordination substrate. The wall is empty until your team joins, so the feature recruits your team for you.
Two presence channels, two defaults. The control-plane presence (the signed heartbeat described under Network posture above) is on by default: hmd presence off silences this repo (<repo>/.heimdall/presence.json → {"enabled": false}), hmd presence off --global is the machine-wide kill switch (~/.heimdall/presence-off), and hmd presence sever is the zero-egress opt-out. The file-based wall heartbeat is separate and off until you ask for it: sentinels/hmd-gate-event.sh writes <repo>/.heimdall/team/<haid>.json (TTL ~30s; a stale file means the agent left) only when <repo>/.heimdall/team/CONSENT exists, and the statusline reads those files only when the server roster is empty. Names in that dir never leave the repo. The watchman watches your gates, not your team. At squad scale the wall caps at the ~6 most-recently-active teammates plus a +N more tail so a wide terminal never wraps.
The deny flash — the clip. When a gate blocks, hmd-gate-anim.sh redraws the big watchman inline: a scanning pulse settling to a green sparkle on pass, or three red beats and ✗ BIFRÖST CLOSED on deny. TTY-only — in CI or a pipe it collapses to one clean final frame so logs stay readable.
bash sentinels/hmd-gate-anim.sh deny "oracle/falsify" $HMD_HAIDWiring (settings.json):
{
"statusLine": {"type":"command","command":"bash ${CLAUDE_PLUGIN_ROOT}/hooks/statusline.sh"},
"subagentStatusLine": {"type":"command","command":"bash ${CLAUDE_PLUGIN_ROOT}/sentinels/hmd-subagent-statusline.sh"}
}install.sh wires this for you — it registers both entries into your ~/.claude/settings.json (honoring $CLAUDE_CONFIG_DIR) using the absolute installed path, idempotently and without clobbering a statusLine you set yourself. The ${CLAUDE_PLUGIN_ROOT} form above is the plugin-hook spelling; a user-level statusLine resolves no such variable, so the installer fills in the resolved absolute path — which is why the HUD now reaches every dev, not just whoever hand-wired it in dev setup.
Cursor CLI gets the identical renderer through its own plug-in point, registered automatically by hmd init — mechanics and the honest limits of that second host are in Also gates Cursor CLI below.
hooks/statusline.sh drives the full-width watchman and falls back to the legacy single line if python3 is missing — it never errors, never blocks. Already a ccstatusline (9.2k★) user? Keep your line and drop the watchman in as a Custom Command widget:
python3 sentinels/hmd-statusline.py --widget # just the watchman + verdict segmentThe sigil ships solo-first (viral-cheap, no team required); the watch wall is the team-gated headline that lights up once presence is wired into your gate hooks.
hmd is itself a Claude Code plugin, but the gate it wires into git is not plugin-specific. Once a repo has been through hmd init, code written by Cursor CLI's agent (cursor-agent) is checked by the exact same pre-commit/pre-push → bin/heimdall-gate-run path as code written by Claude Code — same stub-scan, same falsify oracle, same BIFRÖST deny on the same terms. Cursor also reads AGENTS.md at session start the way Claude Code reads CLAUDE.md, so the fenced hmd init block that tells an agent "this repo is gated" reaches it too.
hmd init also auto-registers the watchman HUD into Cursor's own ~/.cursor/cli-config.json statusLine the moment it detects cursor-agent on PATH (bin/heimdall-statusline-register-cursor, verbs status / register / unregister, also reachable as hmd cursor-statusline). It is idempotent, never clobbers a statusLine you set yourself, and never runs at all against a machine that has never touched Cursor. Opt out with HEIMDALL_NO_CURSOR_STATUSLINE_REGISTER=1 or ~/.heimdall/no-cursor-statusline-register (the shared HEIMDALL_NO_STATUSLINE_REGISTER=1 / ~/.heimdall/no-statusline-register markers suppress both hosts).
hmd's own sub-agent spawns can delegate work to more than one detected CLI backend. heimdall-ai-select list --auth shows what is on PATH, and /hmd:switch-ai (or heimdall-ai-select select <letter|id>) changes which one hmd prefers, persisted to .planning/settings.json under ai_backend with its provenance (user vs auto-default). That preference is not a live hot-swap: hmd is Claude Code code running inside a Claude Code process, and nothing restarts that process into a different CLI mid-session — switching takes effect for delegated spawns now and for the next session's default, never for the session you're already in.
The gate itself has the same teeth under Cursor as under Claude Code — nothing here weakens what gets blocked. What differs is when and how it fires:
- Caught at commit, not at write. Claude Code's live
PreToolUsehook blocks a stub the instant it is written. Cursor CLI has no equivalent pre-write hook, so the same stub is instead caught atgit commitbybin/heimdall-gate-run's backstop — later, but the same gate, and the commit still does not land. - No SessionStart automation. The auto-update check,
cc-selfheal, the resume probe, the dream notice, the presence keeper, andheimdall-ai-select session-startall fire from Claude Code's SessionStart hook. Cursor has no such hook for hmd to fire from, sohmd initis a one-time, by-hand setup rather than something that re-arms itself every session. - No SessionEnd automation. Checkpoint writes,
verify-edits, and auto-commit likewise never fire under Cursor — there is no session-end hook to fire them from. - No subagent orchestration. hmd's
Agent/SendMessagespawning is Claude Agent SDK-specific; it does not run inside acursor-agentprocess. - Cursor's own
.cursor/hooks.jsonis untouched. Cursor has a separate, unrelated hook mechanism (sessionStart/preToolUse/beforeShellExecution, allow/ask with an exit-code-2 deny); hmd does not populate it and has no plan to.
cd /path/to/your/project
heimdall --auto "build a real-time dashboard with auth and charts"--auto runs Claude Code's auto permission mode (--permission-mode auto): a background safety classifier that blocks prompt injection and risky escalation. It is not the default. With no flag, heimdall launches Claude Code with --dangerously-skip-permissions (full autonomy) — bin/heimdall sets PERMISSION_FLAG="--dangerously-skip-permissions" and only a leading --auto overrides it. Pass --auto as the first argument for the safer opt-in; run the bare default only where you would accept a fully autonomous agent.
Live flagship status: evals/flagship/STATUS.md — the ❌ rows are kept in view. The corpus dip log and golden provenance are at evals/corpus/CORPUS-STATUS.md and evals/oracles/emulator-gb/fixtures/golden/VERIFICATION.md.
A verification system that can't show you its own failures can't be trusted with yours.
- Stack packs (
skills/stacks/) — teach Heimdall a framework's conventions and build commands. - Oracle packs (
evals/oracles/) — add a falsifiable external gate for a new domain.
See CHANGELOG.md for release history.
hmd keeps itself and its host current, in the background, on session start — both are throttled (~24h), detached (never block the session), idempotent, and opt-out:
- Plugin auto-update (
bin/heimdall-autoupdate): checks the installed version vs the latest GitHub release; if newer, re-runs the latest installer in the background (takes effect next launch; never hot-swaps the running session). Off:HEIMDALL_NO_AUTOUPDATE=1or~/.heimdall/no-autoupdate. - Claude Code self-heal (
bin/heimdall-cc-selfheal): on a NATIVE Claude Code install, auto-repairs the "✘ Auto-update failed" class — a stale npm-global@anthropic-ai/claude-codeconflicting with the native updater. It removes ONLY that conflicting package, ensuresautoUpdates:true, and re-runsclaude update. Never touches an npm/brew-managed install, never uninstalls anything else, never touches credentials. Off:HEIMDALL_NO_SELFHEAL=1or~/.heimdall/no-selfheal. Inspect:heimdall-cc-selfheal status. - Default module reconciliation (same updater): compares the installed modules
against the default set. A module whose class requires consent is never acquired here —
it is named, with the
hmd modules addcommand to run. Off:HEIMDALL_NO_MODULES=1or~/.heimdall/modules-optout. Inspect:heimdall-autoupdate status.