DEV Community

Vansh Sharma
Vansh Sharma

Posted on AI-assisted

Why Terminal AI Agents Need Structural Shell Guardrails

Why Terminal AI Agents Need Structural Shell Guardrails

AI coding agents are increasingly useful because they can do more than suggest code: they can run tests, install dependencies, inspect Git state, and modify a repository.

That power creates a practical trust problem.

A developer may be comfortable with an agent running cargo test, but not with it running git reset --hard, reading .env, force-pushing a branch, or piping decoded content into a shell.

The obvious response is often “put the agent in a cloud sandbox.” That can be useful, but it is not always the best first layer. Local development tools need low startup latency, access to the existing repository, and an approval workflow developers can understand.

I built BlastGuard as a local-first safety layer for terminal-using coding agents.

The goal: make the blast radius visible

BlastGuard intercepts a proposed Bash command and returns one of three decisions:

  • allow for recognized low-risk commands;
  • ask when a developer should approve the action;
  • block for high-confidence dangerous patterns.

The important part is that the decision is explainable. A developer should be able to see the requested command, affected paths, likely network activity, and whether the work is isolated in a disposable Git worktree.

Why regex alone is not enough

Shell commands are not simple strings.

A regex can spot rm -rf, but agents can use pipelines, command substitutions, wrappers, redirects, subshells, and decoded payloads. For example, a command can hide execution behind a pipeline:

echo "..." | base64 -d | sh
Enter fullscreen mode Exit fullscreen mode

BlastGuard uses Tree-sitter to inspect Bash structure: pipelines, chains, redirects, substitutions, subshells, and common wrappers such as env or bash -c.

This does not make static analysis perfect. Shell behavior can still depend on aliases, functions, environment expansion, scripts, and arbitrary binaries. When BlastGuard cannot interpret behavior reliably, its safer answer is ask, not false confidence.

Git worktrees provide a useful rollback boundary

Command analysis alone does not protect a developer’s repository state.

BlastGuard creates an isolated Git worktree for an agent session. The original repository must begin clean and at a known commit. The agent’s changes stay in the worktree until the developer chooses one of two actions:

  • accept: apply the complete Git-visible patch back to the source repository as staged changes;
  • reject: remove the managed worktree and leave the source repository untouched.

This is intentionally a Git-state guarantee, not an OS-security guarantee. It cannot reverse network calls, ignored files, credential access, external writes, or malicious binaries.

Controlled execution needs process cleanup

A timed-out shell is not necessarily a stopped shell.

A command can create background descendants that continue running after the direct process exits. BlastGuard’s controlled execution path starts Bash in a dedicated Unix process group and targets the whole group on timeout or output-limit exhaustion.

That behavior is backed by integration tests, including tests for background descendants and unbounded output. It is one of the details that makes terminal safety tooling harder than simply calling Command::spawn().

What BlastGuard is—and is not

BlastGuard is:

  • a developer-visible approval layer;
  • a structural Bash policy engine;
  • a secret-redaction layer for its own output;
  • a disposable Git-worktree workflow;
  • a local CLI with Claude Code hook support.

BlastGuard is not:

  • a complete OS sandbox;
  • protection against arbitrary native binaries;
  • a guarantee against all secret formats or dynamic shell behavior;
  • a replacement for container, VM, or kernel-level isolation.

I think this distinction matters. Developers need useful guardrails now, but safety claims should be precise.

BlastGuard is open source, written in Rust, and available here:

https://github.com/Vansh0Sharma/blastguard

I’m looking for feedback from developers who use terminal agents: what command, secret, Git, or rollback scenarios would you want a local guardrail to handle?

Top comments (1)

Collapse
 
pm25coder profile image
pm25coder •

Answering the closing question with the scenario that bit me hardest.

A guardrail can be the reason a command failed, and the exit status cannot say so. I run an agent's commands through matched confinement tiers on Windows Server 2022. Under one tier the shell died at load time — the loader refused before a single line of the command ran. Under the other it started, ran, and then failed. To the caller both arrived as a bare non-zero status with no text: the same shape. Since the policy layer had already classified that command as fine, the natural reading was "your command failed", and that sent me debugging the command rather than the boundary for a while.

What fixed the attribution was not decoding the status. The runner now prints one sentinel line immediately before it spawns the real command, so sentinel missing plus non-zero exit means "none of this ran" — a third outcome that an exit code can never carry. Presence of a token is a cheaper and more honest signal than interpretation of a number, and it is the only one that survives a wrapper swallowing the real status.

I raise it because it is the same axis as your "a timed-out shell is not necessarily a stopped shell": a tool that can prevent a command from starting owes the caller an answer about whether it did, and the command's own exit status is not that answer.

Second one, smaller, on the rollback axis: a boundary is only as wide as the paths it can see resolved. I had a write that looked inside the workspace escape it — the harness pinned one spelling of the user's home while the write went through the other (environment variable versus OS profile) — and the guard's own test passed the whole time. The adversarial test that catches it is to write through a ~-shaped path with the home variable pointed at a decoy and assert the boundary sees the write land.

One process note, and I am deliberately keeping it to that: your SECURITY.md asks for reports in its scope to arrive through private vulnerability reporting, and that path is not currently enabled on the repository — the fallback the document names is a personal address. I have an observation in the areas that document covers and no route to send it through, so it is staying out of a public thread. Enabling that feature would give reporters the path the policy already promises them.