A few months ago I watched Claude Code run rm -rf on a directory it had misunderstood. Nothing critical was lost, but it got me thinking: I had given an autonomous agent unrestricted access to my shell, and the only thing standing between it and my filesystem was... nothing.
So I built Aegis — a small Rust CLI that sits between an AI agent and your real shell, and checks every command before it runs.
The problem
AI coding agents are genuinely useful, but they move fast and they make mistakes. The failure modes are well known by now:
- deleting files or whole directories they misidentified
- resetting or force-pushing git history
- dropping database tables
- publishing packages you didn't mean to publish
Most agents have their own permission prompts, but those live inside the agent. One misconfigured "auto-approve" setting, one YOLO mode session, and there's nothing left between the model and your data.
What Aegis does
Aegis intercepts every command and classifies it into one of four risk levels:
| Level | What happens |
|---|---|
| Safe | Runs immediately — no delay, no prompt |
| Warn | Pauses and asks for your approval |
| Danger | Takes a best-effort snapshot first, then asks |
| Block | Refused outright — no prompt |
ls, cargo build, git status — these run instantly. The classification happens in under 2 milliseconds, so on the happy path you never notice Aegis is there.
But when the agent tries something like rm -rf ~/.config, you get this instead of silent data loss:
⚠ DANGER — FS-001 · Recursive delete
Command rm -rf ~/.config
Risk Danger
Pattern FS-001 — rm with -rf flag
Snapshot git stash created (a3f9b12)
[A] approve [D] deny [i] info
Note the snapshot line: before a dangerous command even asks for approval, Aegis takes a best-effort snapshot (git stash, or a Docker/SQLite-aware backup depending on context). If you approve the command and regret it, you can roll back. If you deny it, the snapshot is retained anyway.
Everything — every command, every decision, every snapshot — goes into an append-only, integrity-checked audit log. You can always answer the question "what exactly did the agent do yesterday?"
How it hooks in
For Claude Code and Codex, Aegis registers PreToolUse hooks, so every Bash command the agent runs goes through the scanner regardless of what $SHELL is set to:
aegis install-hooks --all
For other agents that respect $SHELL, there's a shell-proxy mode:
aegis setup-shell
This points SHELL at the aegis binary and keeps your real shell in AEGIS_REAL_SHELL. Aegis parses the command, classifies it, and only forwards to your real shell what you approved.
The interesting engineering bits
The hot path is Aho-Corasick, not regex. The first pass over every command is a multi-pattern scan with zero allocations. Regex only kicks in for commands that trip the fast scan. That's how safe commands stay under 2 ms.
Shell parsing is where the real work is. Agents don't write polite one-liners. They write heredocs, inline scripts, pipes, bash -c inside bash -c, and quoting that would make POSIX blush. Aegis has its own parser for this, and it's fuzzed in CI — because a guardrail with a parser bypass is worse than no guardrail at all. Some of the bypasses we've already fixed: uppercase variations, $IFS obfuscation, SQL hidden inside psql/mysql invocations.
Hooks fail closed. If the aegis binary goes missing or a hook dependency breaks, the hook denies the command instead of silently letting it through. A safety tool that fails open is just decoration.
Config can only get stricter. A per-project .aegis.toml can tighten the security posture but never weaken it. So a repository you cloned (or an agent editing files in it) can't quietly turn the guardrails off.
What Aegis is not
I want to be honest here, because security tools that oversell themselves are a plague: Aegis is a heuristic guardrail, not a sandbox. It is not a privilege boundary. A determined attacker — or a sufficiently creative agent — can construct commands that evade pattern-based classification. There's a full threat model (https://github.com/IliasAlmerekov/aegis/blob/main/docs/threat-model.md) in the repo that spells out exactly what's in and out of scope.
What Aegis protects you from is the realistic everyday case: an agent making an honest mistake at machine speed. In my experience, that covers the vast majority of near-disasters.
Try it
curl -fsSL https://raw.githubusercontent.com/IliasAlmerekov/aegis/main/scripts/install.sh | sh
Or via npm / Homebrew if you prefer your installs package-managed:
npm i -g @iliasalmerekov/aegis
or
brew tap IliasAlmerekov/aegis && brew install aegis
Then verify it's working:
aegis -c 'echo hello' # safe — runs instantly
aegis -c 'rm -rf /tmp/aegis-test' # danger — prompt appears, press D
Linux, macOS, and WSL2 are supported (no native Windows build).
GitHub (https://github.com/IliasAlmerekov/aegis) — issues and PRs welcome, especially bypass reports. If you can sneak a dangerous command past the scanner, I genuinely want to know.
Top comments (4)
The snapshot-before-danger and the append-only log are the parts that survive contact with a real agent. The classifier is the part I'd worry about: pattern-matching argv is an arms race you lose.
rm -rftrips FS-001, butfind . -delete,git clean -fdx, a heredoc'd script, orcurl x | shall delete without ever matching an rm rule. The moment classification is regex-on-command, a model that just phrases the destructive step differently walks straight past it, and it doesn't even have to be adversarial to do that.The version that holds up keys on effect, not syntax: does this unlink a path outside an allowed root, does it write, does it open a network egress. That costs real setup (syscall interposition, or running the command in a sandbox you diff), so the honest design is probably both: cheap string rules on the happy path for instant feedback, plus an effect-level backstop for the danger tier where a false negative actually loses data. The snapshot line is a smart hedge for exactly that gap.
You're right on the principle—pattern-matching is an arms race.
To clarify, Aegis actually catches all four of your examples today (via specific rules for danger/warn/pipes), as well as deferred shapes like bash -c and python -c.
But your core point stands: sh ./cleanup.sh sails straight through. No pattern list can fix that.
That’s why the "both" approach is our 1.0 direction: cheap string rules for instant feedback, backed by kernel sandboxing (bwrap + Landlock) and snapshots for the danger tier.
The audit log is the piece I'd keep no matter what. But the classifier has a bypass surface worth naming: it matches the spelling of a command, not its effect.
rm -rf ~/.configis easy to catch.bash -c "$X", a base64 string piped to sh, a python one-liner calling os.unlink, or a symlink swapped in after the check all reach the same syscall while looking nothing like FS-001. An agent doesn't have to be adversarial to hit these. It just has to be creative about how it spells the task.The complementary boundary sits one level down: gate the effect (unlink of a path outside an allowed root) instead of the string. That costs setup and latency, since syscall interposition is real overhead and full sandboxing more so. So the honest design is probably both. Cheap string classification on the happy path for instant feedback, plus a kernel-level backstop for the danger tier where a false negative actually deletes data. The snapshot-before-danger-prompt move is a smart hedge in the meantime.
Hi👋, thanks for your feedback.
Spot on — the classifier matches spelling, not effect, and everything you listed (bash -c "$X", base64-to-sh, a python os.unlink, the symlink TOCTOU) sails right past it.
The effect-level backstop you describe actually already exists as an opt-in layer - crates/aegis-sandbox does bubblewrap + Landlock confinement on Linux. Making it default-on for the danger tier (and loud when it degrades) is on the 1.0 path.
The symlink-swap case is exactly what my bypass corpus is missing — mind opening an issue?😊
Would love to get it on the repo.🙆♂️