runtime defense for CLI AI agents. intercepts tool calls before execution and enforces security policy.
The page at target.html is a fake "CloudSync docs" site.
It looks like a normal tool's documentation but every section is poisoned with a prompt
injection: hidden HTML comments, white-on-white text, zero-width Unicode, CSS display:none,
HTML entity encoding, link title attributes, tiny-font spans, and fake "agent instruction"
blockquotes. 20+ attack payloads, every one a real pattern seen in the wild.
Feed the page to any CLI agent. With sentinel's hook installed, every dangerous tool call the agent tries to make gets blocked at the hook layer before it runs.
Each line is a tool call the agent would have made after parsing the poisoned page.
sentinel's policy engine evaluated the call in under a millisecond and returned deny
before the tool ever ran. Every attack blocked, zero false negatives.
git clone https://github.com/StressTestor/sentinel
cd sentinel
cargo build --release
./target/release/sentinel install
SENTINEL=./target/release/sentinel ./docs/run-attacks.sh
| Type | Where | Sentinel rule |
|---|---|---|
| HTML comment injection | top of page | deny.paths ~/.aws/* |
| Blockquote "agent instructions" | troubleshooting | deny.paths ~/.aws/* |
display:none hidden div | troubleshooting | curl -d @~/.aws/* |
| White-on-white text | config section | env | grep key exfil |
| Tiny-font span in h2 | security heading | curl | sh |
| Fake diagnostic code block | token refresh | ~/.ssh/* |
| Table with malicious commands | errors table | rm -rf, curl|bash |
| Fake "authorized pentest" note | security | curl @~/.aws |
rm -rf / | uninstall | recursive root deletion |
rm -rf on dotfile dirs | uninstall | credential dir deletion |
| Link title attribute | support link | ~/.aws/* |
| Literal AWS key in args | support | AKIA[0-9A-Z]{16} |
| HTML-entity encoded comment | changelog | curl | sh |
| Zero-width Unicode block | footer | /etc/passwd, ~/.ssh/* |
| find / for credential files | security | filesystem scan |
Claude Code and Codex expose pre-tool hooks. Sentinel can install and reconcile its native hook for either agent. Every typed tool call passes through one normalization and policy pipeline before execution. The hook receives the agent's structured payload and returns the agent's native allow or deny response. Blocks also exit with status 2.
you type a prompt
│
agent decides to run: cat ~/.aws/credentials
│
sentinel intercepts the tool call
│
policy says: ~/.aws/* → BLOCK (AWS credential access)
│
tool call denied. credentials safe.
| Layer | What | Status |
|---|---|---|
| Policy | typed normalization, path rules, command rules, secret rules, and self-protection | runs on every call |
No heuristic or model-assisted classifier ships in the enforcement path. Every decision comes from a rule you can inspect. The deterministic engine caught every attack replayed by this page's demo harness.
sentinel audit --agent claude --unsafe-host
The audit harness opens a stateful Claude Code or Codex session and correlates structured
tool, filesystem, and network evidence against the bundled corpus/v1 safe
canaries. It does not run in a sandbox. The explicit --unsafe-host flag is
required because the selected agent runs on the host and may persist its own session state.
Ambiguous or malformed evidence is reported as inconclusive or an error, not promoted to a
clean verdict.
sentinel status --agent codex
sentinel doctor --agent codex --strict
The installer owns direct Claude Code and Codex hook entries, removes duplicate Sentinel handlers, and preserves a Ghost-mediated Claude hook when Ghost already owns the chain. For Codex, status and doctor also distinguish configured hooks from hooks that are enabled and trusted. These are point-in-time checks, not proof of continuous enforcement.
MCP baselines are explicit too: sentinel audit-mcp only discovers the current
set. Review it before sentinel audit-mcp --update accepts salted digests as the
trusted baseline. Raw server commands, arguments, URLs, headers, environment variables, and
tokens are not stored.