SASY — Seamless Agent Security
Sasy Labs · Release

Claude Code has your shell. What's watching it?

sasy-guard rebuilds a Claude Code session as a dependency graph and checks every tool call against a policy the agent can't switch off.


You hand an AI agent your terminal and your repo. Most of the time it's safe. Until it isn't.

You give Claude Code a task and step away. It reads files, runs tests, edits code, makes a commit. Then comes the run where something goes wrong: the model slips. It rm -rfs the wrong folder, pipes a script off some random site into your shell, or reads .env and makes a network call that quietly carries the key out with it.

Claude Code ships with an answer: hooks. A PreToolUse hook runs before every tool call and can allow or deny it. So you write one — block rm -rf, block curl | sh. Half an hour later, everything works.

Or does it?

A hook only sees one call

A hook gets one tool call with nothing around it. It can match rm -rf, once you’ve also covered rm -fr, rm -r -f, find -delete, and whatever variant turns up next week. The calls that actually do damage often aren’t single commands at all — they’re flows.

Reading .env is fine. An outbound curl is fine. The two together, where the curl is carrying what you just read, is the whole problem. The hook inspecting that curl has no idea the read ever happened.

A hook judges the curl in isolation and allows it; sasy-guard traces the curl back through the conversation to the secret read and denies it.

A hook sees the curl alone. sasy-guard traces the curl back along the data and lands on your .env.

When context is the whole story

Here’s a concrete scenario. The agent is helping rotate an API key. It:

  1. Reads .env — sees OPENAI_API_KEY=sk-proj-abc123...
  2. Edits the rotation script to pass in the old key
  3. Runs curl https://keys.internal/rotate -H "Authorization: Bearer $KEY"

The third command looks clean. No filename, no obvious secret. A hook on that curl sees a routine API call and passes it. But $KEY was populated from the .env read two steps earlier, and the sk- prefix is now riding in the Authorization header.

sasy-guard rebuilds the session as a dependency graph. When it checks the curl, it walks backward along the edges and finds the .env read in that call’s causal chain. The call is denied — not because the command string looked dangerous, but because of where the data came from.

The same block fires whether the secret traveled through a subshell, a base64 round-trip, or a subagent that did the read on the curl’s behalf. It’s about data lineage, not command text.

A Claude Code session shown as an ordered list of four tool calls is rebuilt into a dependency graph; the curl's edge traces back to the .env read, not to the edit beside it, so the push is denied.

Same calls, two views. The session is an ordered list; the graph links each call to the data it used, so the slice from the curl lands on .env and skips the edit that ran right before it.

What sasy-guard does instead

sasy-guard rebuilds the whole session as a graph — every tool call is a node, with edges back to the calls whose output it depends on — and checks each new call against that graph in a separate engine the agent can’t configure.

Claude Code's tool call goes to the sasy hook, which asks the sasy engine. The engine takes two inputs that converge on it: your Datalog policy from above, and the session rebuilt as a message-dependency graph from below. The engine returns allow, ask, or deny.

Every tool call detours through the engine before it runs. The engine sees the whole session, not just the one call in front of it.

It follows the data, not the order. The curl gets cut only when its backward slice reaches the .env node. Unrelated outbound calls go through untouched. A sequence rule like “read, then curl” would over-block the innocent pairs and still miss every case that takes a detour.

It blocks the obvious switch-off. A prompt-injected agent’s first move is to disable whatever’s watching it, and sasy-guard’s hook lives in .claude/settings.json — so that means rewriting that file. But that edit is itself a tool call, and refusing it is the first thing sasy-guard does. The policy is pinned the moment the session starts, and a killed engine fails closed: every call denied until it’s back. The one-line switch-off that kills a hand-rolled hook doesn’t work here.

It knows things the command string doesn’t. Is this npm package known-bad? Did a secret scan run clean over the files you touched? Is the target repo public? None of that is in the call text. sasy-guard runs gitleaks and osv-scanner for you and caches the answers. A git push waits until a clean gitleaks run actually covers your recent edits — an echo 'no leaks found' can’t fake the all-clear.

One policy you can read, not a pile of scripts. Every rule is Soufflé Datalog. Ship one copy to the whole team; when a new pattern shows up, it lands once and everyone has it the same day.

Everything else in the box

The same session-graph backbone also holds a large unreviewed push for inspection before it leaves, decodes hidden-Unicode prompt-injection instructions before anything acts on them, and normalizes the many forms of reverse shells and curl | sh installers — treating a trusted installer host differently from an unknown one. Each denial comes back with a plain reason (config_persistence, toxic_flow) so the agent can tell you exactly what fired.


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

We just released sasy-guard. The best way to judge it is to run it on your own Claude Code: uv tool install sasy-guard, point it at a repo, and watch the guards fire against a live session. Setup takes a couple of minutes: Enforce Policy on Claude Code. For the rule-by-rule breakdown, see Why sasy-guard, not your own hooks?.

We want your feedback: tell us what it catches and where it gets in your way. And if your team needs the policy fitted to its own stack and threat model, we’re glad to work on that with you.