A security harness for AI coding agents. Fifteen guards that sit between your agent and your machine — and instructions for building them.
Shown with the policy and rescan config in place — see BUILD-ORDER.md for what to create first.
$ npm install left-pad
⛔ blocked — bare install; prefix it with your scanner
$ sfw npm install left-pad
🔎 scanning… clean
✅ installed
$ sfw npm install shiny-new-thing
⛔ blocked — published 4 hours ago, below your 14-day floor
a package this fresh has not been looked at by anyone yet
$ rm ./dist/bundle.js
✅ runs — a throwaway path, no interruption
$ rm src/index.js
❓ approve? plain rm (file delete)
$ rm -rf build/
⛔ blocked — recursive force-delete
no override; run it yourself if you meant it
$ git push --force
⛔ blocked — force push
# next morning, at session start
🔒 supply chain: N trees, 0 new findings
⚠️ integrity: 1 file changed since last baseline — hooks/bash-scope-guard.shThe first two commands are the point: the gate does not refuse the install, it redirects it. So is rm ./dist/bundle.js further down, which runs untouched. A guard that stops everything gets uninstalled by the end of the week.
Supply chain is the centre of this harness. Six of the fifteen guards defend the install path and what happens to it afterwards, and they are why the rest exists — an agent that can install a package can run a stranger's code on your machine without ever asking you. The other nine are the perimeter around that: what your agent may delete, read, edit and push.
Thirteen of the fifteen need nothing but a shell and jq.
| Tool | Needed by | Cost |
|---|---|---|
jq |
fourteen of the fifteen | a package install |
Socket Firewall (sfw) |
the install guard | free, no account, no quota |
Socket scan (socket) |
the periodic re-scan | free tier, draws on a per-scan quota |
| Aikido Safe Chain | the install guard's second opinion, optional | free |
One setting comes before all of this. Tell your package manager not to run install-time scripts: ignore-scripts=true in npm's user config. Packages are allowed to execute their own code as part of being installed, without a prompt, and that is the route the install guard exists to close. The cost is real — anything that compiles a native component during install will now fail, often unhelpfully. Two things make that workable: the audit hook logs every installed package that wanted to run code, so a failure has a name, and the setting is a default rather than a gate, so --ignore-scripts=false on that one install lets it through once you have decided it is worth it.
Written and tested on macOS. The portability notes in each specification cover the differences that bite on Linux.
Every script in the harness, and one line on what each does. The config files are shown where they feed in; the tables in the next section say the same things at more length.
LAYER 0 ─ shared foundations. Two files the rest lean on.
│
├─ hooks/lib/hook-fired-log.sh
│ every guard appends a line here when it runs; thirteen of them read it
└─ hooks/lib/allowed-prefixes.sh
the paths your agent is allowed to touch — you write this one
LAYER 1 ─ standalone guards. Each stands on its own.
│
├─ workflow-posture-check.sh
│ denies pull_request_target in a workflow. Needs no config at all
├─ holy-files-guard.sh
│ warns before an edit to a file you marked critical
├─ worktree-scope-guard.sh
│ blocks an edit that escapes the worktree you are working in
├─ bash-destructive-guard.sh
│ hard-denies destructive shell and git commands, asks on the rest
└─ git-hooks/pre-push
rejects a push carrying a commit whose author was forged
LAYER 2 ─ scope guards. One allowlist, two surfaces.
│
├─ bash-scope-guard.sh ← allowed-prefixes.sh (owns it)
│ blocks a command that names a path outside your scope
└─ read-scope-guard.sh ← allowed-prefixes.sh
the same boundary for file reads and searches
╔══════════════════════════════════════════════════════════════════════════╗
║ LAYER 3 ─ SUPPLY CHAIN · six guards · the centre of this harness ║
║ The install gate, and everything that keeps looking after the install. ║
║ Two config files feed it, and they are separate. ║
╚══════════════════════════════════════════════════════════════════════════╝
security/supply-chain-policy.json security/supply-chain-
the install gate's rules rescan.config.json
│ which trees get re-scanned
│ │
├──► supply-chain-guard.sh │
│ the install gate for npm and pip. Scanner │
│ prefix, malware scan, minimum age, no │
│ git-URL dependencies │
│ │ │
│ └──► supply-chain-guard-selftest.sh │
│ drives the gate with a known-bad input │
│ every session, and says if it stopped │
│ denying. The smoke it drives lives in │
│ test-supply-chain-guard.sh │
│ │
└──► npm-audit-on-install.sh │
after an install, reports only what is newly │
vulnerable against your own baseline │
│ │
└──► security/audit-diff.sh │
the set-diff maths, shared so there is │
only one copy of it │
│ │
▼ │
supply-chain-rescan-worker.sh ◄───────────────────────┤
re-scans installed trees on a cadence, because │
clean when you installed it is not clean now │
│ │
▼ │
supply-chain-rescan.sh ◄──────────────────────────────┤
dispatches that re-scan at session start and │
reports what it found │
│
non-npm-tree-detector.sh ◄────────────────────────────┘
finds dependency trees nothing is scanning, and
keeps saying so until you decide
LAYER 4 ─ watching the guards themselves.
│
├─ integrity-check.sh
│ fingerprints every file that executes by itself and reports a change
│ ← bash-scope-guard, holy-files-guard, bash-destructive-guard,
│ pre-push, worktree-scope-guard, supply-chain-guard
├─ hook-freshness-audit.sh ← hook-fired-log.sh
│ names the guards that have gone quiet, against a registry you write
└─ observability-self-check.sh ← hook-fired-log.sh
answers "did my guards actually run", because a hook that stops
firing produces no output at all, and tracks whether the audit
above is still running
The same layers, with the order to build them in and the prerequisites each one needs, are in BUILD-ORDER.md. Change one of these two pictures and change the other.
Supply chain — six guards, and the reason for the rest
| Guard | What it does | Needs config |
|---|---|---|
supply-chain-guard |
The install gate. Requires a scanner prefix, runs a malware scan before the real install, enforces a minimum package age, refuses git-URL dependencies, and can route the install through a second, independent feed | a policy file |
supply-chain-guard-selftest |
Drives the gate with a known-bad input every session and confirms it still denies | none |
npm-audit-on-install |
After an install, diffs vulnerabilities against your previous baseline and reports only what is new. Records which lifecycle scripts were skipped | shares the guard's policy + per-repo marker |
supply-chain-rescan |
Dispatches the periodic re-scan at session start and reports its result | shares one config |
supply-chain-rescan-worker |
Re-scans installed trees on a cadence, because a package that was clean when you installed it can be flagged next week | shares one config |
non-npm-tree-detector |
Finds dependency trees on your machine that nothing is scanning, and keeps saying so until you decide | shares one config |
Command safety
| Guard | What it does | Needs config |
|---|---|---|
bash-destructive-guard |
Hard-denies destructive shell and git commands. Asks on the cautionary ones. Splits chained commands so a dangerous fragment cannot hide behind a safe one | optional |
bash-scope-guard |
Blocks a command that names a path outside your working scope | an allowlist you write |
read-scope-guard |
The same boundary for file reads and searches. Guarding writes and not reads leaves your keys, your credentials and other people's code readable | shares the allowlist |
worktree-scope-guard |
Blocks an edit that escapes the worktree you are working in | a marker |
holy-files-guard |
Warns before an edit to a file you marked critical. Pauses outright when something creates a new hook script | a pattern list |
Publishing and identity
| Guard | What it does | Needs config |
|---|---|---|
workflow-posture-check |
Denies any attempt to add a pull_request_target trigger to a workflow. That trigger runs with your repository's secrets while checking out a stranger's code |
none |
git-hooks/pre-push |
Rejects a push carrying a commit whose author was forged. Git accepts any author string you type, without verification | an author list |
Watching the guards
| Guard | What it does | Needs config |
|---|---|---|
integrity-check |
Fingerprints every file on your machine that executes by itself — hooks, agent settings, shell startup files, scheduled jobs — and reports at session start when one changed | none |
observability-self-check |
Answers "did my guards actually run". A hook that silently stops firing produces no output at all, which looks exactly like a quiet, healthy session | a registry you write |
Every specification carries the pass-by-pass logic, the failure rules, the edge cases that bit, and a list of acceptance tests you can run against whatever your agent generated.
Every file under hooks/ is a build specification written for your coding agent. You point your agent at one, it generates the script, in your layout, wired to your config. You review what it wrote.
That is deliberate. Copying a stranger's shell scripts into the path that decides what your agent may delete is a strange thing to do in the name of security. Instructions let you read the reasoning, watch the code appear, and keep a build that fits your machine rather than someone else's.
Each specification carries the exact logic, the failure rules, the edge cases, and a test list you can run against what your agent produced.
Most tools guard the install. This one keeps looking afterwards.
- At install time — a malware scan before the real install, a minimum age so you are not the first person to run a package, and a refusal of git-URL dependencies.
- After the install — a vulnerability diff against what you had before, and a record of the lifecycle scripts that were skipped.
- Days later — a periodic re-scan, because a package that was clean when you installed it can be flagged next week.
- Before a command runs — destructive shell and git commands, reads and writes outside your working scope, edits to files you marked critical, and workflow changes that would hand your repository secrets to an untrusted pull request.
- Always — a fingerprint of every file that runs by itself, so an edit to a guard is reported, and a check that the guards actually fired, because a hook that silently stops running produces no output at all.
Two facts are shared by every guard: what your agent hands a hook, and how a hook answers so the host actually stops the call. Those live once, in HOST-CONTRACT.md, rather than fifteen times. Get either wrong and the guard still builds, still passes its own tests, and blocks nothing.
The parts that took the longest are not the pattern matching. They are these.
- Fail closed, or fail safe, and say which. A guard that denies a tool call denies on any internal error. A session-start notice never blocks a session, whatever happens to it. Each specification states its posture and does not borrow the other one's word because it sounds stronger.
- Every deny gate proves itself. A canary drives each gate with a known-bad input and a known-good one, every session, and reports if either answer changed. The pairing is the point: a gate that denies everything passes a positive-only test while being useless.
- A failed scan is never reported as clean. Coverage is tracked separately from findings. A timeout, a refused request or a quota limit means not scanned, which is a different word from nothing found.
- It must not cry wolf. A guard that fires on ordinary work gets switched off, and then you have neither the guard nor the belief that guards like it are workable. False positives are recorded and tuned, not tolerated.
- Notice, do not act. The parts that watch and record never change anything. Only the gates decide.
- Deny wins across a whole command. Every install in a chained command has to clear every gate. Checking only the first one is how a scanned install carries an unscanned one along behind it.
- A warning nobody sees is not a warning. An advisory emitted in the wrong shape is discarded by the host in silence: the hook fires, prints correctly, exits zero, and reaches nobody. The specification for this states the delivery rule as a fail rule and tests the consumer, not the producer.
- No override where one would be asked for. A gate whose escape hatch can be requested by an instruction your agent read somewhere is not a gate. Where a bypass genuinely has to exist, it takes a real advisory identifier and it is logged.
Read BUILD-ORDER.md first. Several guards share files, and building them in the wrong order produces a stack that runs, reports healthy, and defends less than you think.
If you want one useful thing today, build hooks/workflow-posture-check.md. It needs no config and no third-party tool, and it blocks a well-known route for stealing a repository's secrets.
Version 1 gates the npm, npx and pip commands, and not every install path receives every check. Other clients for the same registries — pnpm, yarn, bun, uv, poetry, pipx — reach them ungated; wider command coverage is planned for version 2.
This is the successor to claude-code-safety-hooks, which published five of these guards as working scripts. That repository was a snapshot; this one is the whole harness, and it publishes the reasoning rather than the code.
Built for Claude Code and its hook configuration. The concepts carry to any agent host with a point where tool calls can be intercepted.
A specification that would build a guard weaker than it claims is worth reporting privately rather than in an issue. SECURITY.md says what counts, what does not, and where to send it.
MIT. See LICENSE.