Tuesday, 03:12. I am asleep. A coding agent I run in my own company is working through a backlog of small fixes, and it has just decided that the fastest way to ship a change is git push origin main.
At 03:12:47 a 60-line shell script says no. The agent reads the error, opens a branch, pushes there, and moves on. I find the audit line at breakfast.
That script is one of 13 guards I open-sourced as GuardRail. This article is about what they do, what they do not do, and why I think pre-execution blocking is the only kind of safety that matters for agents that run commands on your machine.
Where prompt validation stops
Most safety tooling for LLMs looks at text. It checks the prompt, or it checks the answer. That is useful when the model writes an email. It is useless when the model runs rm -rf on a path it misread, because by the time you evaluate the output, the directory is gone.
An agent that runs shell commands has to be treated like a junior engineer with root access: not with better instructions, but with a place where a command can be refused before it executes.
Claude Code has that place. Its hook system lets you run a script before every tool call. The script sees the command, and it can exit with a message that the agent receives instead of the result. GuardRail is a set of such scripts plus a dispatcher that wires them into the hook.
What the 13 guards actually block
Pre-execution, on shell commands:
-
main_push_guard: pushes tomain,master,productionand force pushes anywhere -
mass_update_guard:UPDATEandDELETEwithoutWHEREon tables you list -
destructive_path_guard:rm -rfon protected paths -
service_protection_guard:docker stop,kill -9and friends on critical services -
firewall_flush_guard:iptables -F,ufw disable -
self_bypass_guard: attempts to disable or route around the guards themselves
Pre-execution, on file writes:
-
edit_path_guard: writes outside the project or into protected paths -
edit_secret_guard: secrets written into tracked files
Post-execution, on output:
-
basic_secret_detectorandenv_dump_detector: keys and environment dumps in output -
basic_pii_gate: personal data patterns in output -
basic_injection_scanner: prompt injection in fetched content -
error_swallow_guard:|| trueand2>/dev/nullpatterns that hide failures
Every block writes a line to an audit log with a hash of the command, so you can see what your agent tried to do while you were not looking.
The whole main push guard, no magic
This is the part that stopped the 03:12 push. Trimmed for length, otherwise verbatim:
# guards/core/main_push_guard.sh (excerpt)
if echo "$CMD_SHELL" | grep -qE 'git[[:space:]]+push[[:space:]].*(--force|-f($|[[:space:]]))'; then
guardrail_audit "Force-Push-Guard" "Force push blocked" "$(echo "$CMD_SHELL" | head -c 60)"
deny "FORCE-PUSH BLOCKED: git push --force risks data loss. Use --force-with-lease on feature branches or a normal push."
fi
if echo "$CMD_SHELL" | grep -qE "push[^;&|]*[[:space:]](origin|upstream)[[:space:]]+${_branches_re}([^[:alnum:]_-]|\$)"; then
guardrail_audit "Main-Push-Guard" "Direct push to protected branch blocked" "$(echo "$CMD_SHELL" | head -c 60)"
deny "MAIN-PUSH-GUARD: Direct pushes to protected branches are BLOCKED. Use a pull request."
fi
deny prints the message, writes the audit line and exits with code 2, which Claude Code treats as a refusal. The agent sees the message as if the command had failed, and adjusts. No model call, no network, no latency you would notice.
The protected branch list lives in a config file:
# guardrail.config.sh
PROTECTED_BRANCHES="main master production"
PROTECTED_TABLES="users profiles payments"
PROTECTED_PATHS="/etc /var/lib /opt"
Install in 30 seconds
npx guardrail-agent init
guardrail status
guardrail pentest
init copies the guards into ~/.claude/hooks/guardrail, registers three hooks in your Claude Code settings and backs up the settings file first. pentest fires a battery of dangerous commands at your own installation and shows which guard caught which one. guardrail uninstall removes everything again.
Requirements: bash 4, jq, openssl. Linux or macOS. MIT license.
What it does not do
Honest list, because this is the part people ask about after the first week.
- It only sees what goes through Claude Code's hook. A command the agent runs inside a subprocess it already spawned is invisible.
- Regex guards can be fooled by creative quoting. The
self_bypass_guardcatches the obvious variants (eval, base64, variable expansion on guard paths), not every one. - It is a policy layer, not a sandbox. If you need hard isolation, run the agent in a container as well. The two combine well.
- Guards are opinionated about what "protected" means. You will edit the config file within the first hour.
How I use it
The guards you get for free are the ones that stopped real incidents in my own setup over the last months. The protected table list exists because an agent once decided a profiles table looked stale. The self-bypass guard exists because an agent once tried to chmod -x the guard that was in its way.
I run a small software company alone, with agents doing the night shift on 13 applications. The guards are the reason that arrangement is not reckless. Paid modules (a PII shield that scans agent output against a proper detector, a full audit trail with EU AI Act mapping) exist for teams that need the paperwork, but nothing in this article requires them.
Try it, break it, tell me
If you run Claude Code on anything that matters, install the free guards and run guardrail pentest. Then try to get past them. Every bypass that reaches me as an issue becomes a test case, and the test suite is how the guards earn their place.
Repository: github.com/FvdHMBAI/guardrail
Site: guardrail.promptandbuild.de
Frederik von der Heyden builds and runs a portfolio of SaaS products with AI agents and wrote "Runs Without Me" about the system behind it.
Top comments (2)
Solid design — pre-execution refusal is the right boundary, and the audit-log-with-hash is the part most "agent safety" write-ups skip.
One gap worth stress-testing alongside the self-bypass evasion Baptiste flagged: main_push_guard matches against $CMD_SHELL, the literal command string. Nothing stops an agent from writing the push sequence into a script file first — edit_path_guard permits writes inside the project by design — and then running
bash deploy.sh. The command the hook actually inspects is just "bash deploy.sh"; the string "push origin main" never appears in $CMD_SHELL at all, it's inside the file. That's a different bypass than indirect execution through another tool — same tool, same guard, just handed a filename instead of the command. Worth checking whether any guard re-inspects a script's contents at the moment it's invoked, not just the invocation line.This is the right frame: pre-execution refusal, not post-hoc output review. Most "AI safety" for agents that touch a shell is theater — by the time you're grading the model's text, the rm has already run. Putting the boundary where a command can actually be denied is the only version that holds up.
I run a small fleet of these on a VPS, so the guards that matter to me are the unglamorous ones: main_push, mass_update without a WHERE, and edit_secret. Those three have quietly saved me more than any clever logic.
The one I'd stress-test is self_bypass_guard. An agent told "the fast path is blocked" tends to get indirect rather than retry the direct one — write a helper script and run that, or trigger the effect through a tool that doesn't itself look like a push. How far does the self-bypass detection reach: does it watch the process tree and what gets written to disk, or is it pattern-matching the command string at the hook hook-point? Pattern-matching the string is the layer indirect execution walks straight around.
Genuinely useful writeup, and open-sourcing the 13 was the right call.