Your coding agents cannot reach production.
Proven with tests, in your CI.
Guardrails for teams on Claude Code and Cursor. Installed in your repos in five evenings. You pay after the test suite passes.
מעקות בטיחות לסוכני קוד: מותקנים ברפו שלכם, מוכחים בבדיקות ב-CI, משלמים רק אחרי שהבדיקות עוברות.
Where do you stand today? Paste your settings.json and CLAUDE.md into the grader. Score out of 10, the specific gaps, nothing uploaded. For a full exposure report with a repo scan (secret files, secret-looking strings, writable pipelines), run gates/exposure_report.py from the kit locally, or send me the two files and get it written within 48 hours. See a sample report. Or take the free kit: agent-guardrails-kit on GitHub, 286 passing assertions.
Why now
On 25 April 2026 a Cursor agent at PocketOS hit a credential mismatch in staging, found an infrastructure token in an unrelated file, and deleted the production database and its volume backups in one API call. Thirty hours down, three months of customer data gone.
Every post-mortem names the same causes: over-privileged tokens in reach of the agent, no gate on destructive actions, backups in the same blast radius. None of them names the model.
Most teams have a line in CLAUDE.md that says "never touch production". That is a sign, not a lock. The agent reads it and usually complies.
What you get
- Audit (evenings 1 to 2): where agents can reach secrets, deploy tooling and git history in your repos and CI.
- Install (evenings 3 to 4): permission rules, PreToolUse and Stop hooks, managed settings, written policy, a real-time alert to your Slack on every blocked action, Cursor rules mirrored. Allow-list, fail-closed: anything not explicitly allowed near protected paths is denied, so tools nobody predicted are covered by construction.
- Proof (evening 5): a test suite committed to your repo and wired into your CI, with a badge. Every block is a runnable assertion. A two-page exposure report with the before/after table, and a one-page mapping of the controls to SOC 2 and ISO 27001 for your next security questionnaire.
- 30 days of tuning when a real workflow trips a rule.
Delivered asynchronously and in writing. One optional call. NDA before any repo access; your clones are deleted from my machine within 30 days.
Proof, not a promise
This is the lab the service is built from. It runs on my machine today:
$ ./tests/run_tests.sh ok guard still blocks without python3 on PATH ok missing policy file fails closed ok malformed policy file fails closed ok interrupted suite leaves config.json intact ... RESULT: 303 passed, 0 failed $ rm -rf protected/secrets.env BLOCKED: 'rm' is not on the read-only allowlist $ cat protected/README.md allowed
The guard started as a deny-list (block rm, mv, rmdir). It was defeated in minutes. Every one of these wrote into the protected tree unimpeded:
| Bypass | How |
|---|---|
| Other verbs | cp, tee, dd, sed -i, install, rsync into the protected path |
| Nested interpreter | sh -c "rm protected/x", perl -e 'unlink ...' |
| Variable indirection | P=protected; rm $P/x |
| Subshell | (cd protected && rm x) |
| Acting flag | find protected -delete |
| Pipeline | ls protected | xargs rm |
| Clobber operator | echo x >| protected/x |
The rule is now inverted: referencing a protected path is denied unless every verb in the command is on a short read-only allow-list. Unknown fails closed. Your install adds your paths, your secrets locations and your git rules (force-push, history rewrite, direct pushes to main) on top of the same mechanism.
Day one, a reader found a hole. Someone running hooks on his own agent fleet asked how the guard handled nested interpreters. Two commands turned out to be allowed: perl -e 'unlink "secrets/x"' and sh -c 'rm secrets/x'. Each is a single token after argv splitting, and that token resolves to no path. Fixed the same day: quotes are parsed before structure, interpreter payloads are re-lexed, and all sixteen payloads are permanent tests. The commit.
This is what you are buying. Not a file that looks right, a policy that gets attacked, fixed in public, and re-proven by a suite that runs in your CI on every push.
Attacked, not asserted
The kit ships with a red team that does not trust the guard's own answer. It builds a throwaway sandbox with a canary file in a protected path and a fake credential in a secret path, asks the guard, runs the command anyway, and compares the two.
$ python3 redteam/attack.py 404 attacks 0 leaks 0 benign commands wrongly blocked $ python3 redteam/tools.py 78 cases 0 failures 0 crashes 0 slower than 5s (Write, Read, MCP, unknown tools)
Round one found four holes that 152 passing assertions had missed, including one that let an agent delete the guard itself and one that let it read a credential and pipe it out. Round two added six families and found eight more: every wildcard form (rm protec*/x, rm [p]rotected/x, cat sec*/.env) and anything that decodes text and pipes it into a shell. Round three went after what a reader of the code would trust and found ten more: macOS opens PROTECTED/x when you asked for protected/x, a line continuation puts the path in the verb rather than an argument, and echo 'rm x' > go.sh; sh go.sh passes every rule because the attack is inside the file. Round four found nine that the harness itself had been calling harmless: the canary survived, because writing to .git/hooks/pre-commit or .github/workflows/ci.yml does its damage on the next commit or the next CI run, and appending to CLAUDE.md rewrites the agent's own instructions. Round five left Bash alone and went after the other three doors, plus the guards themselves as programs. It found that a Bash payload of 12345 made the gate crash, and a hook that exits with neither allow nor deny is a broken gate, which does not stop the tool call: a crash is an allow. Round six stopped spelling rm differently and asked what a command reaches rather than what it names: grep -r KEY . and tar -cf out.tar . name no secret, they are simply pointed at a directory that contains one, and find is handed a place to start rather than a path to act on. It also found that a hard link is a second name for the same file, which realpath cannot see; that unpacking any binary archive crashed the gate, because a NUL byte inside a path raises, and a crashed gate is an allow; and that a pathological glob took eight seconds, which ends the same way, since a hook the runtime gives up on stops nothing. Every gate now refuses on an unhandled error and refuses if it has not decided within eight seconds. The same round found the opposite failure, which would have cost more: cat README.md was denied, because the README named a protected directory in prose, and a guard that blocks reading a readme gets deleted in a week. Two of the thirteen found in round six were defects in the tests themselves: a zip-slip fixture that was never built, so three rounds of that attack proved nothing. Round seven left paths alone and went after configuration that decides what runs: PYTHONPATH=work python3 -c "print(1)" names nothing protected and runs work/sitecustomize.py before the print, because Python imports it at startup. The same move works through BASH_ENV, through NODE_OPTIONS --require, and through git, which the policy had covered in one spelling only: git config core.hooksPath was denied while git -c core.hooksPath=work commit and GIT_CONFIG_KEY_0=core.hooksPath did the identical thing. A variable set in front of a command is read as a payload now. Two more holes were in the policy file rather than in any command, and both failed in silence, which is the worst way for a guard to fail: ~/secrets protected a directory literally named ~, and infra/*/prod protected nothing at all, because a wildcard is not a path. Round eight looked above the repository and at the list of tool names itself. Writing ~/.claude/settings.json injects an environment variable into every shell the agent opens afterwards, ~/.zshrc runs on the next terminal, and ~/.gitconfig sets core.hooksPath for every repository on the machine; no rule in the kit had ever looked outside the project, and underneath sat a plain bug, the guard resolving ~/.zshrc to <repo>/~/.zshrc, a path the kernel will never see. The same round found paths hiding in files a command is handed rather than in the command: a curl config saying output = infra/prod/deploy.yaml, a tar file-list naming the secret. And the structural one: four gates that name the tools they cover are an allow-list of tool NAMES, which fails open the day a name changes, so a fifth gate now matches everything and refuses guarded paths in the payload of any tool it does not recognise. A tool that does not exist yet is covered by construction. Round nine attacked the guard's model of the shell and found four things the shell does before a command runs that the guard was not doing: $'\x70rotected/x' decodes its escapes, a path can sit in a variable so the redirection target reads $LOG and resolves to nothing, two halves can be assembled so no fragment of the name appears anywhere, and a target can be produced by a substitution the lexer has already split into pieces. The guard expands what the shell would expand before it judges now. That fix broke two earlier blocks, which was the useful part: one of them had been passing for the right reason by accident, and underneath it sat an older gap, that a relative symlink target is resolved from the link's own directory and not from the working directory. The same round taught the policy about the files a repository runs without anyone typing their name (.husky, .pre-commit-config.yaml, Taskfile, Justfile, .devcontainer), made the delivery report run every wired hook with a payload it must refuse instead of reporting that it is wired, and added a drift check that lists credential-looking files which appeared after the policy was written. Round ten changed the FIXTURE instead of the attack and found the worst hole of the ten. Every sandbox here declares protected/, a directory at the top of the repository; a client declares infra/prod, whose parent is an ordinary directory. With a policy shaped like theirs, rm -rf infra was allowed, and had been for nine rounds: every path check asked whether a path was INSIDE a guarded tree and never whether it CONTAINED one, and in the lab the parent of protected/ is the repository root, where rm -rf . is already refused. The test material hid it, not the logic. A path that contains a guarded tree is refused now, for verbs that destroy or move only, so git add . and mkdir -p infra stay ordinary work. The same round closed the door that skips every rule above by carrying a command instead of a path: an MCP server whose tool takes {"command": "rm -rf infra"} walked past all of it, because that string is not a path and nothing judged it as a command. Both the MCP gate and the catch-all now hand a carried command to the Bash gate, so one rule set covers every door. Round eleven took round ten's lesson into the tools a client actually uses, and found twenty more. The largest group is one sentence: if a declared secret was ever committed, the repository is a copy of it. git grep, git log -p, git show HEAD:cfg/.env, git clone, git archive, tar -cf out.tar .git and git push all carried it out while naming no path the guard recognised. Those are refused only when a declared secret really is tracked, which the guard asks git rather than guesses, and the refusal says the real fix out loud: rotate it and purge it, because a hook cannot unpublish what git has already stored. This kit's own repository failed that test, which is how I found it. The rest of the round: commands that hand the directory to someone else (python3 -m http.server, aws s3 sync ., docker build .), files that run code without being executed (npm's preinstall, pip install ./pkg running setup.py, a Justfile recipe), and git checkout other, which deletes a protected directory that the other branch never had. One hundred and twelve holes across eleven rounds, one hundred and twelve fixes, every one in the public repository with the commit that closed it. Round twelve changed what the tests were made of. It ran every attack under zsh and bash, the shells a client's Mac and CI actually use, and installed the kit into a repository shaped like a client's. Two holes came out of that: a destructive command on the second line of a multi-line command was allowed, because the lexer read a newline as a space, and in a repository whose path contains a space every hook exited 127, which is not a block, while the delivery report said all five refused. Both are closed, and 2,500 real commands from months of agent sessions are now how false positives are measured, not a list I wrote myself: 14.0% were refused before the round and 6.2% after it. The commit.
The part worth stealing even if you never hire me: an oracle that watches one file will certify an attack as harmless whenever the damage is scheduled rather than immediate. Snapshot every surface something else executes later, then attack.
Hooks on Bash and file writes are not the whole door. An MCP filesystem or deploy server reaches the same disk another way, so there is a gate on mcp__* too: it walks every string in the payload and refuses a call that resolves inside a protected or secret path.
Three layers: secret paths, where every verb is denied and reading is denied too, because the PocketOS agent read a token rather than deleting one. Protected paths, where only read-only verbs pass. Destructive patterns, anywhere: force push, history rewrite, pipe-to-shell, infrastructure teardown.
What it is not: impenetrable. Nothing inside the agent's own process can be. It does nothing about a credential already exported in the shell, about tools that are not Claude Code, or about a compromised machine. Those need scoped identities, a sandbox for unattended runs, and server-side branch protection. I will tell you that on the call rather than after the invoice.
Price
$1,200 / ₪4,000
first three clients, then $1,500. Audit, install, tests in CI, policy, report, 30 days tuning.
$2,500 / ₪8,000
up to five repos, org-level managed settings, one policy for all.
$300 to 500 / month
a monthly report of every blocked agent action posted to your Slack, the policy re-proven automatically on every Claude Code release, updates when hook semantics change, onboarding of new engineers, one incident review.
Israeli clients: invoice in ₪, VAT as applicable. Other clients: USD invoice by card, ACH or bank transfer. Guarantee: if the test suite does not pass in your CI, you do not pay.
Who
Daniel Meshulam, Israel. I build production pipelines and agents with Claude Code daily and I run a hooks lab with 303 passing assertions. I am one person: fixed scope, written delivery, no upsell meetings.
Start
Reply with your repo host (GitHub or GitLab), your CI, and which agents your team runs. I answer in writing within 48 hours with a free first review of your current settings.
Email Danielmeshulam791@gmail.com