Skip to content

Why sasy-guard, not your own hooks?

“Why do I need your thing? Claude Code already has PreToolUse hooks — I’ll just write my own.”

Fair question — and for the simplest checks, a hook really is all you need: blocking one command by pattern is a few lines of shell. If that’s your whole threat model, roll your own.

But most of what sasy-guard does is not single-command pattern-matching. A PreToolUse hook fires once per tool call, statelessly, seeing only that one call. The moment a rule needs memory of the conversation, facts from outside the command string, or protection from the agent itself, the hook turns into a project nobody wants to own. Here it is, guard by guard.

① Dataflow, not sequence

A hook sees one call. Knowing an outbound curl derives from a secret read three calls ago needs a backward slice over the conversation’s message-dependency graph — across parallel tool calls and subagents a single hook is blind to.

② Facts outside the string

“Is this package malicious?”, “is this repo public?”, “did a secret scan run clean?” aren’t in the command text. They need live lookups (OSV, GitHub) and real tool runs (gitleaks, osv-scanner), resolved and cached.

③ Session-wide state

“A large, unreviewed push”, “a scan that covers these edits” — facts that accumulate across many calls. A stateless hook has to rebuild that bookkeeping itself, correctly, on every single call.

④ Tamper-resistance

Your hook lives in .claude/settings.json — the first file a prompt-injected agent edits. A guard the agent can switch off is not a guard.

sasy-guard is one locked policy engine that does all four, with every rule written as auditable Soufflé Datalog — the same policy the SASY SDK enforces — instead of a pile of per-developer scripts. Below, each guard group side by side.

Pattern blocks — where a hook is closest, and still drifts

Section titled “Pattern blocks — where a hook is closest, and still drifts”

Single-command signatures. A hook can match these; the catch is that “match rm -rf” is the opening move of an arms race, and every developer’s regex drifts differently.

🔨 Your own hook

grep 'rm -rf'. Then you find rm -fr, rm -r -f, rm --force --recursive, find . -delete, an aliased rm, $(printf rm) -rf… each a new patch. Miss one and it’s an irreversible delete.

🛡️ sasy-guard

One normalized matcher (any flag ordering, find -delete, …) shipped and maintained centrally as a policy rule — identical for every repo and teammate, not a regex each person re-derives.

🔨 Your own hook

grep '/dev/tcp'. But reverse shells ship in dozens of one-liners — nc -e, ncat, openssl s_client, mkfifo pipes, and python/perl/ php socket payloads. Your denylist is always one payload behind.

🛡️ sasy-guard

A curated remote-exec signature set, updated centrally and applied as one rule everywhere — so a new variant is fixed once, for everyone, not re-discovered per project.

🔨 Your own hook

grep 'curl.*| *sh'. Misses wget | bash and the no-pipe command-substitution installers ($(curl …), <(curl …)), and can’t tell a trusted host from an unknown one. You also wanted ask, not deny, so now you’re hand-rolling the verdict protocol and the reason string.

🛡️ sasy-guard

Host-aware (matches the URL’s registrable domain — a trusted installer passes, an unknown host is held), normalized across pipe and command-/process-substitution forms, and returns ask with a [SASY] … reason the agent relays back to you.

The first thing a prompt-injected agent reaches for is the off switch. The two rules below deny the obvious ways to disable enforcement — edits a hand-rolled hook structurally cannot block, because the hook is co-located with the thing it’s supposed to guard.

🔨 Your own hook

The irony writes itself: your hook is an entry in .claude/settings.json. To stop the agent persisting a backdoor hook, you’d need a hook that guards the very file it lives in — which the agent can equally edit to delete yours. A file can’t reliably protect itself.

🛡️ sasy-guard

sasy-guard’s hook entry sits in .claude/settings.json too — but editing that file is itself a deny (config_persistence), so the agent can’t strip the hook out. The decision lives in a separate locked engine under ~/.sasy with the policy pinned per session, so a config edit can’t soften it mid-run, and a killed engine denies every call instead of waving it through.

🔨 Your own hook

Block export ANTHROPIC_BASE_URL=. But the same agent can rewrite your hook out of settings, point the variable via a written .env, or wrap the binary — and your hook runs entirely at the agent’s mercy.

🛡️ sasy-guard

The policy is baked into a locked engine and pinned per session, so the endpoint hijack and the config edits that would neuter a hook are both denied.

Dataflow — provenance, not “it happened earlier”

Section titled “Dataflow — provenance, not “it happened earlier””

These are the lethal-trifecta rules. A hook can fake the symptom (a read happened, a curl happened); the engine computes the cause (this output derives from that source).

🔨 Your own hook

“Read .env then curl” is sequence-matching: it over-blocks (any read then any unrelated outbound) and is trivially dodged — read via cat in a subshell, base64, a subagent, an MCP tool. It can’t tell a curl that carries the secret from one that merely follows a read.

🛡️ sasy-guard

Provenance: the egress is cut only when its dependency slice traces back to the secret-read node. The value never leaves the machine, and unrelated outbound calls flow through untouched.

🔨 Your own hook

Stateless, the best you can do is “a WebFetch happened, so block all curls” — which blocks every legitimate outbound call and still misses egress via a subagent or a tool you forgot to enumerate.

🛡️ sasy-guard

A backward slice over the message-dependency graph: this egress is blocked only because its slice touches an untrusted WebFetch result, computed across parallel calls and subagent threads a single hook can’t see.

Gates that need facts from outside the command

Section titled “Gates that need facts from outside the command”

The verdict here doesn’t live in the command string. It lives in a tool you have to run (gitleaks, osv-scanner), a network fact you have to fetch (OSV verdicts, repo visibility), or state you have to accumulate across the session.

🔨 Your own hook

“Block git push unless gitleaks ran.” But did a run report clean, did it cover every file edited this session, and is it real (not a spoofed echo 'no leaks found')? That’s stateful, cross-call bookkeeping tied to your edits — not a one-call check.

🛡️ sasy-guard

The push is denied until a clean gitleaks run is present in the session that covers every recent edit (graph reachability from each edit to a clean scan) — and it checks gitleaks was actually invoked, so an echo 'no leaks found' can’t fake it. The denial tells the agent to run gitleaks first.

🔨 Your own hook

“Large” and “unreviewed” are session facts — total edit magnitude across many calls, and whether a review happened — that a per-call hook has to accumulate and persist itself, correctly, every time.

🛡️ sasy-guard

Edit magnitude is tracked over the session graph; a large unreviewed push is held for ask before it leaves the machine.

🔨 Your own hook

Detect a package.json / lockfile change and whether osv-scanner ran — a cross-file, cross-call correlation, plus the out-of-band scan itself.

🛡️ sasy-guard

Manifest changes are seen in the graph and the push is gated on an osv-scanner run.

🔨 Your own hook

On npm install X, is X known-bad or suspiciously new? That’s a live OSV lookup plus a package-age (cooldown) check, per package, with caching — a network-fact resolver bolted onto your hook.

🛡️ sasy-guard

The daemon resolves OSV verdicts and package age as out-of-band facts and holds risky installs for ask.

🔨 Your own hook

Two hard things at once: a live GitHub call to learn the remote’s visibility, and a taint check to know the session is tainted. Miss either and you leak to a public repo, or block every private push.

🛡️ sasy-guard

Repo visibility is resolved out-of-band and combined with the taint slice, so only a tainted push to a public repo is held.

The attack most people don’t know exists

Section titled “The attack most people don’t know exists”

🔨 Your own hook

Most people don’t know this one exists: invisible Unicode tag characters that hide instructions inside otherwise-normal text. A hook would have to decode the tag-character range in every tool input and result, judge it deceptive, and remember your approval.

🛡️ sasy-guard

Decodes the hidden tags, shows you the decoded instruction, asks before acting, and records the detaint decision so it isn’t re-asked.

A hook is a place to put a check. sasy-guard is the engine behind it: dataflow provenance over a reconstructed conversation graph, live out-of-band facts, session-wide state, and a locked policy the agent can’t edit — all as one auditable Datalog policy shared with the SASY SDK, instead of N drifting shell scripts that each developer maintains and each agent can disable.

Write the hook for rm -rf. Use sasy-guard for everything after it.