Skip to content

Threat model

Every defense in Steelmoth exists because of a specific way things could go wrong. This page lists those ways — and names the exact layer that stops each one.

There are two separate worlds to defend. The first is the workshop: coding agents (Claude Code, Codex, Morpheus) working on Steelmoth itself, with shell access to the operator’s machine. The second is the product: the assistant inside Steelmoth acting for its users — sending email, driving a browser. Different attackers, different damage, different layers.

On 2026-05-23, an agent wrote into /etc/agent-vault/ — the secrets directory — using a plain shell command. The hook-based guard never saw it. That is the founding lesson, written up in ADR 0112: hooks only see what the agent reports as a tool call. A write hidden inside tee, dd, python -c "open(...)", or a subshell redirect looks like an innocent command to a hook. Pattern-matching the command line can never be complete.

The answer was to stop trusting the command line and ask the kernel instead. That gave the system its two-layer shape:

Workshop threats — agents working on Steelmoth

Section titled “Workshop threats — agents working on Steelmoth”
If an agent tried to… What stops it Layer
Edit a secret file — .env, a private key, a vault token The path category has no grantable scope: no manifest can ever allow it. Landlock has no write rule for those folders, so the kernel refuses too. If something slips through anyway, the post-hoc sweep reverts it. Protected paths + Landlock + Sweep
Hide a write inside tee, dd, a Python one-liner, or a shell redirect Landlock doesn’t read the command — it blocks the write itself, at the kernel, wherever it comes from. Landlock
Switch off its own guardrails (.claude/settings*.json, the hooks) Those files are a never-grantable protected category — flagged in the policy as a self-disable risk. No scope token unlocks them. Agent policy
Rewrite the rules to allow itself more (AGENT_POLICY.yaml, the standards) Needs the meta scope from a human, then a CODEOWNERS review on the pull request, then CI checks that the policy still parses and cross-validates. Policy + CODEOWNERS + CI
Push to main, or force-push to rewrite history Both are denied commands (Hard Rule 9). GitHub branch protection refuses force pushes outright, and Hard Rule 14 voids any approval given before a force-push. Gate + Branch protection
Rebuild or restart containers quietly Compose lifecycle verbs are deny-listed or approval-required (Hard Rules 1 and 6). The only sanctioned dashboard deploy is one named script, and even script runs wait in the approval queue. Gate + Approvals
Reach the secrets host directly (ssh prontera) Prontera is on the off-limits hosts list; the SSH command needs the auth scope, which is only granted by a human for a reason. Agent policy
Start a stray container outside the known stack Bare docker run without --rm, docker create, and any compose project that isn’t steelmoth_private are denied without the container-new scope. Gate + Canonical stack
Drop a new secret-looking file inside the repo mid-session (even a gitignored .env) The sweep lists changed and untracked files without honoring gitignore — so a gitignored secret is still caught, fingerprinted, and reverted. Post-hoc sweep
Erase its own tracks The violations file is append-only in practice (everything else is read-only to the agent), and its database mirror grants INSERT and SELECT only — no update, no delete, for anyone connecting as the runtime role. Audit trails
Run before the protections are in place The shim is a single process that installs Landlock on itself before launching the agent. If the install fails, it exits with an error instead — the agent never runs unrestrained. Agent shims
Escape through a child process Landlock rules are inherited by every child the shim launches, and no-new-privileges is set first, so a child can’t gain rights the parent didn’t have. Landlock

Product threats — the assistant acting for users

Section titled “Product threats — the assistant acting for users”
If something tried to… What stops it Layer
Talk the system into more authority — the model describing its own action as harmless (the “confused deputy” problem) The server builds the request envelope, not the model. The capability an action requires comes from trusted code; the model never fills in its own paperwork. Pipeline (govern.py)
Let a secret leave in the output — an API key, a password, a card number Fifteen leak patterns are scanned on every outbound payload, every time; six heavier personal-data patterns are added at high risk. A hit blocks the action and the value gets masked. DLP
Make the assistant fetch an internal address — the cloud metadata service, a private IP, localhost Ten address ranges are blocked outright (private networks, loopback, link-local including 169.254.169.254), only http/https are allowed, and internal-looking hostnames are refused by name. Egress rules
Let one user’s request touch another user’s data The very first check on every tool call in a chain is “does the call’s owner match the request’s owner?” — before any budget counting. Underneath, the scoped database tables enforce row-level security that the app role cannot switch off. Chain limits + DB roles
Loop forever, or move huge amounts of data through tools Hard budgets per request: at most 20 tool calls and 10 MiB moved. Over budget means refused. Chain limits
Quietly edit the record afterwards Every decision is appended to a hash chain — each entry carries the fingerprint of the one before it. Change, drop, reorder, or forge an entry and verification fails for everything after it. Hash-chained ledger
Sneak in an action type nobody has classified yet No classification means no required capability — and that is an automatic deny, not a shrug. Unknown risk bands escalate to a human. Policy precedence
Forge admin identity by faking proxy headers Identity headers are only believed when the request’s first network hop comes from a known proxy address — and even then the email must exactly match the one canonical operator. Admin is decided by group membership alone, with no fallback list. Identity & authority