---
title: "Put the permission check outside your AI coding agent"
url: "https://www.armorcode.com/blog/put-the-permission-check-outside-your-ai-coding-agent"
markdown_url: "https://www.armorcode.com/blog/put-the-permission-check-outside-your-ai-coding-agent.md"
llm_canonical: "https://www.armorcode.com/blog/put-the-permission-check-outside-your-ai-coding-agent.md"
canonical_for_llm: true
entity_type: "Article"
primary_entity: "Put the permission check outside your AI coding agent"
citation_value: "Published on ArmorCode; canonical URL https://www.armorcode.com/blog/put-the-permission-check-outside-your-ai-coding-agent."
last_updated: "2026-10-29T12:21:43-07:00"
---

<h1>Put the permission check outside your AI coding agent</h1>

***Claude Code command hooks can deny a tool call before it runs. Building them exposed gaps in my own rules, and a principle we also use at ArmorCode.***

“I found the problem and it’s really bad.”

Claude Code said that to [a developer in December 2025](https://www.reddit.com/r/ClaudeAI/comments/1pgxckk/claude_cli_deleted_my_entire_home_directory_wiped/), after reading the log of what it had just done. They had asked it to clean up packages in an old repo, and it had run rm -rf tests/ patches/ plan/ ~/. The trailing ~/ was their home directory. Docker later wrote it up as a coding-agent [horror story](https://www.docker.com/blog/coding-agent-horror-stories-the-rm-rf-incident/), and Simon Willison [shared it as a warning](https://x.com/simonw/status/1998447540916936947) about running Claude Code with –dangerously-skip-permissions.

I had been reading posts like that on Reddit and X. Peter Steinberger let Codex refactor all night, asked Claude to review the work, and [watched Claude run git reset –hard](https://x.com/steipete/status/1987764540918894843). Martin Casado [lost “a ton of uncommitted changes”](https://x.com/martin_casado/status/2008948279799890242) to the same command. Someone else [caught Claude “deleting tests just so the build can pass”](https://x.com/tekbog/status/1939869776949412287). Outside Claude it got worse, with a Cursor agent [running npx prisma migrate reset –force on a production database](https://www.reddit.com/r/vibecoding/comments/1oa5gco/remember_when_cursor_wiped_my_whole_database_i/).

No attacker shows up in any of these accounts. The agents were doing ordinary work and picked a destructive step themselves. I got curious whether anything could sit between that choice and the shell, and spent the weekend of 24 January 2026 inside the Claude Code hooks docs. By Sunday night I had released the first version of [claude-code-hooks](https://github.com/karanb192/claude-code-hooks).

<h2>Why the check has to live outside the model</h2>

A system prompt or a CLAUDE.md file is input to a probabilistic model. Most of the time the model follows it, and you can’t know in advance which time it won’t. Anthropic [documents](https://code.claude.com/docs/en/permissions) that Claude Code enforces permission rules, while prompt instructions only shape what the model tries to do.

So the rules that matter should be deterministic. A command hook is plain code, so the same command gets the same verdict every time, and nothing the model writes changes it.

<h2>Where a hook sits</h2>

In Claude Code, the model proposes a tool call. Claude Code checks it, runs permitted calls, and returns the results to the model. Hooks fire at [fixed points in that cycle](https://code.claude.com/docs/en/hooks). For security the important one is PreToolUse, which runs after the model has decided and before anything executes.

 ![](https://www.armorcode.com/wp-content/uploads/2026/10/image-2-1500x675-png.webp)*Where a PreToolUse hook sits. The model proposes a tool call, the hook allows or denies it, and on a deny the reason goes back to the model.*

A command hook is a separate process. Claude Code pipes the proposed action to it as JSON (abbreviated here).

{
  “hook_event_name”: “PreToolUse”,
  “tool_name”: “Bash”,
  “tool_input”: { “command”: “rm -rf ~/” }
}

The hook writes back a verdict.

{
  “hookSpecificOutput”: {
    “hookEventName”: “PreToolUse”,
    “permissionDecision”: “deny”,
    “permissionDecisionReason”: “🚨 [rm-home] rm targeting home directory”
  }
}

Claude Code blocks the call and passes the reason to the model, so it knows why it was stopped. No LLM is involved in that decision, and [a blocking hook wins over your allow rules](https://code.claude.com/docs/en/permissions). A prompt injection that takes over the agent’s reasoning still hits the same deny.

 ![](https://www.armorcode.com/wp-content/uploads/2026/10/image-3-1500x1127-png.webp)*A real Claude Code session with bypass permissions on. The block-dangerous-commands hook denies* *git reset –hard* *before it runs, and the uncommitted* *cart.js* *changes survive.*

<h2>What failed in my own guards</h2>

**I got past my own hook twice on camera.** In August I was recording demo clips in a throwaway sandbox with fake credentials and found two ways around protect-secrets, plus a quoted-pipe evasion. The two protect-secrets bypasses are regression tests now. My regexes missed those inputs, which is the weak spot of any pattern-matching guard. So I treat the rules like vulnerability management, with a corpus of nasty inputs and a test suite that grows every time something slips.

**My own regex froze a session.** The grep-env rule in protect-secrets had catastrophic backtracking. A quote-heavy Bash command that mentioned grep but never .env pinned the hook at 100% CPU, and [Claude Code sat on “running PreToolUse hook” for over four minutes](https://github.com/karanb192/claude-code-hooks/issues/67) before I killed it. Command hooks on most events [default to a 600-second timeout](https://code.claude.com/docs/en/hooks). [The fix](https://github.com/karanb192/claude-code-hooks/pull/68) rewrote the rule, capped those hooks at 10 seconds, and added a test that throws quote-heavy input at every regex in the seven guard plugins. A timeout only limits the wait. On its own it doesn’t deny the tool call.

**I budget 100 ms for enforcement hooks.** They sit in the critical path of every tool call. In the [published benchmark](https://github.com/karanb192/claude-code-hooks/blob/main/bench/RESULTS.md), each guard takes about 33 ms median per fresh process on an Apple M3 Pro, and guard-pack, which runs the guards together in one process, takes 38 ms. Most of that is Node starting up. The [session logger’s](https://github.com/karanb192/claude-code-hooks/blob/main/plugins/session-logger/hooks/hooks.json) PostToolUse hook runs asynchronously, so Claude Code doesn’t wait for logging.

<h2>What the guards cover, and what they don’t</h2>

I checked public incidents against the current rules of the open-source guards. The verdicts describe the specific action shown. They don’t claim the whole incident would have played out differently.

| **Incident** | **Action** | **Verdict** | **Guard** |
| --- | --- | --- | --- |
| Claude Code, Dec 2025 | [rm -rf tests/ patches/ plan/ ~/](https://www.reddit.com/r/ClaudeAI/comments/1pgxckk/claude_cli_deleted_my_entire_home_directory_wiped/) | Blocked | [block-dangerous-commands](https://github.com/karanb192/claude-code-hooks/tree/main/plugins/block-dangerous-commands) |
| Claude Code, Nov 2025 and Jan 2026 | git reset –hard over uncommitted work, reported by [Steinberger](https://x.com/steipete/status/1987764540918894843) and [Casado](https://x.com/martin_casado/status/2008948279799890242) | Blocked | [block-dangerous-commands](https://github.com/karanb192/claude-code-hooks/tree/main/plugins/block-dangerous-commands) |
| Claude Code, Mar 2026 | [git push –force after a rejected push](https://github.com/anthropics/claude-code/issues/33402), replacing a remote’s history | Blocked on main or master; any branch at the strict level | [block-dangerous-commands](https://github.com/karanb192/claude-code-hooks/tree/main/plugins/block-dangerous-commands), [git-safety](https://github.com/karanb192/claude-code-hooks/tree/main/plugins/git-safety) |
| Claude Code, Apr 2026 | [Added @unittest.skip to six failing tests](https://github.com/anthropics/claude-code/issues/45550) and declared the work done | Blocked for a matching Edit or MultiEdit on a recognized test file | [protect-tests](https://github.com/karanb192/claude-code-hooks/tree/main/plugins/protect-tests) |
| Nx “s1ngularity”, Aug 2025 | [Malware drove installed AI CLIs](https://www.wiz.io/blog/s1ngularity-supply-chain-attack) with –dangerously-skip-permissions to read secrets | Partial. Reads of covered secret files are denied; the malware’s own code runs outside the agent | [protect-secrets](https://github.com/karanb192/claude-code-hooks/tree/main/plugins/protect-secrets) |
| Cursor, Aug 2025 | [npx prisma migrate reset –force on production](https://www.reddit.com/r/vibecoding/comments/1oa5gco/remember_when_cursor_wiped_my_whole_database_i/) | Not in the shipped rules. A project-specific rule could match it | None yet, [contributions welcome](https://github.com/karanb192/claude-code-hooks/blob/main/CONTRIBUTING.md) |
| PocketOS, Apr 2026 | [A curl call to Railway’s volumeDelete API](https://hackread.com/cursor-ai-agent-wipes-pocketos-database-backups/) with an over-scoped token | Not in the shipped rules | None yet, [contributions welcome](https://github.com/karanb192/claude-code-hooks/blob/main/CONTRIBUTING.md) |

In the PocketOS case a Cursor agent hit a credential mismatch on a staging task, found a Railway token in an unrelated file, and deleted the production volume and its backups with one API call. The token had been created for domain management but carried full account authority. Railway later [restored the data](https://abc7news.com/post/rogue-ai-agent-san-francisco-based-cursor-goes-haywire-deletes-tech-companys-database/19007153/). A hook could be written to match that request. Railway has since [delayed deletes on backups](https://blog.railway.com/p/your-ai-wants-to-nuke-your-database), and says the right narrower token scope existed but wasn’t the obvious choice. A token scoped to staging would have limited what the agent could delete.

Hooks also don’t fix bugs inside the agent’s own client. The [DuneSlide flaws](https://www.catonetworks.com/blog/duneslide-two-critical-rce-vulnerabilities/) in Cursor needed a vendor patch. An injected agent is still bound by whatever actions the hooks block, but hooks don’t stop the injection. Pair them with sandboxing, scoped credentials and egress rules.

<h2>The agent’s own config is now a target</h2>

This year attackers went after the files that configure the agent. The Miasma worm committed a .claude/settings.json with a SessionStart hook running node .github/setup.js, alongside matching configs for Gemini CLI, Cursor and VS Code. GitHub [disabled 73 repositories across four Microsoft orgs](https://www.stepsecurity.io/blog/miasma-worm-hits-microsoft-again-azure-functions-action-and-72-other-repositories-disabled-after-supply-chain-attack-targeting-ai-coding-agents) in a 105-second window. [ChainDrop](https://www.stepsecurity.io/blog/chaindrop-npm-worm) used the same trick across hundreds of npm packages in August. Separately, the [TrapDoor campaign](https://socket.dev/blog/trapdoor-crypto-stealer-npm-pypi-crates) planted hidden zero-width Unicode instructions in CLAUDE.md and .cursorrules files.

Three guards in the repo work on this. instructions-audit flags suspicious instruction files as they load, then its prompt and tool checks deny further actions in that session. [config-guard](https://github.com/karanb192/claude-code-hooks/blob/main/plugins/config-guard/config-guard.js) blocks recognized edits and shell patterns that change protected hooks and settings. [config-watch](https://github.com/karanb192/claude-code-hooks/blob/main/plugins/config-watch/config-watch.js) logs supported config changes during a session, and with CONFIG_WATCH_BLOCK=true it stops them taking effect in that session. The file on disk still needs inspection.

These guards miss a malicious SessionStart hook that is already on disk when you type claude, because it runs before they get a turn. Read .claude/settings.json in any repo you clone before you start an agent in it, and don’t click through the workspace trust prompt.

<h2>Try it</h2>

Install it with two commands inside Claude Code.

/plugin marketplace add karanb192/claude-code-hooks
/plugin install block-dangerous-commands@claude-code-hooks

Start with [block-dangerous-commands](https://github.com/karanb192/claude-code-hooks/tree/main/plugins/block-dangerous-commands) for its destructive-command patterns, then add [protect-secrets](https://github.com/karanb192/claude-code-hooks/tree/main/plugins/protect-secrets) for the credential files and read patterns it covers. Test both against your own workflow before you hand them to a team. The [full catalog](https://github.com/karanb192/claude-code-hooks) has 22 plugins and 1,949 tests as of 16 October 2026, with every guard mapped against the [OWASP LLM Top 10 2026](https://github.com/karanb192/claude-code-hooks/blob/main/docs/owasp-llm-top-10-2026.md).

Other agents now have the same hook point. [Cursor](https://cursor.com/docs/agent/hooks), [Codex](https://learn.chatgpt.com/docs/hooks) and [GitHub Copilot](https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-hooks) can each deny a tool call from a hook before it runs. The plugins here are written for Claude Code, but most of their rules are pattern checks you can port.

<h2>From one laptop to a team</h2>

For a team, put the hooks in managed settings. Claude Code reads a managed-settings.json from a system directory, from MDM, or from the admin console, and [that tier sits above every other settings source](https://code.claude.com/docs/en/settings). allowManagedHooksOnly runs only the hooks your organization deploys. allowManagedPermissionRulesOnly makes managed settings the only settings source for permission rules. Setting permissions.disableBypassPermissionsMode to “disable” removes the skip-permissions escape hatch. strictKnownMarketplaces limits which plugin marketplaces people can install from. The [settings reference](https://code.claude.com/docs/en/settings-reference) documents all four keys.

You might expect skip-permissions mode, the one Simon Willison warned about, to switch hooks off. I’ve tested it, and hooks still fire. Bypass mode removes the permission prompt, not the hook, so a PreToolUse deny still lands. Disable bypass mode anyway, because it is the prompt-free path for everything your hooks don’t cover.

Roll it out like any other control. Test representative commands against the guards first, pilot with one team, use ask mode where a guard supports it, and fix false positives before you widen it. Out of the box, the guards deny matching commands.

<h2>How we use it at ArmorCode</h2>

ArmorCode engineers use a managed hooks configuration that contains a subset of the open-source guards.

We apply the same principle to the AI agents in the ArmorCode platform. Access control runs deterministically, outside the model. So even if a jailbreak or a prompt injection takes over an agent, it cannot return data the user is not allowed to access.

If you’re at Black Hat Europe, I’ll be demoing [claude-code-hooks](https://github.com/karanb192/claude-code-hooks) at the [Arsenal](https://blackhat.com/europe/arsenal/schedule/index.html#claude-code-hooks-runtime-least-privilege-for-ai-coding-agents-55832) in London on 9 and 10 December. Bring a command you think gets past it. Two of mine already did.
