I built an open-source tool called PolicyApprovalGate. It applies deterministic local rules immediately before Claude Code or Codex CLI runs a shell command. It also checks Claude Code's direct Read, Write, and Edit operations and target paths extracted from Codex apply_patch calls.
Updated August 14, 2026: The first version of this article covered shell commands only. PolicyApprovalGate now applies path policies to Claude Code's Read, Write, and Edit tools and to target paths in Codex
apply_patchcalls. The installation and limitations below have been updated as well.
nobuo-miura
/
PolicyApprovalGate
A rule-based PreToolUse hook for Claude Code and Codex CLI that denies dangerous Bash commands, asks for confirmation on risky ones, and audits every call — no AI/LLM required.
PolicyApprovalGate
PolicyApprovalGate is a PreToolUse hook that checks shell commands against local rules before Claude Code or Codex CLI runs them. On Claude Code, it also checks direct file access through Read, Write, and Edit.
It detects dangerous commands, pushes to protected branches, and access to out-of-project or sensitive paths, then records the decision in an audit log. It does not use AI or an LLM, so the same input always produces the same result.
Important
PolicyApprovalGate complements the host permission model, sandbox, and human review. It is neither a complete shell analyzer nor a security boundary, and should not be your only line of defense.
In this example, PolicyApprovalGate blocks Claude Code from reading ~/.ssh and returns the reason to the host.
Features
- Deterministic local decisions without AI or an LLM
- Always-on protection against recursive force-deletion of the filesystem root or current user's home
- Configurable command…
Why I Built It
I started this project after Claude Code tried to run a command that I had explicitly told it not to run in CLAUDE.md.
CLAUDE.md is useful for communicating project policies, coding conventions, and preferred workflows. However, writing a prohibition there does not make it an enforceable rule at execution time.
The official Claude Code documentation describes CLAUDE.md as instructions loaded into the model's context, not as a hard enforcement layer. Instructions may not always be followed exactly, especially when they are ambiguous or conflict with one another.
This was particularly concerning in Auto mode, which reduces confirmation prompts so that Claude Code can work on longer tasks with fewer interruptions.
Auto mode currently uses a separate classifier to review each tool call. It also takes CLAUDE.md into account, but the documentation states that this does not guarantee safety.
I therefore decided to add a separate check that runs immediately before a supported tool operation is executed.
I wanted it to do four things:
- Immediately reject commands that must never be executed
- Require confirmation for selected commands, even when the rest of the task can proceed automatically
- Require confirmation before accessing sensitive data
- Require confirmation for operations outside specified directories
That is why I built PolicyApprovalGate.
What Is PolicyApprovalGate?
PolicyApprovalGate is a policy gate written in Go that runs as a PreToolUse hook for Claude Code and Codex CLI.
When an agent attempts a supported operation, PolicyApprovalGate receives the tool call through standard input and checks it against the configured rules. Shell commands are checked against command rules and path policies. Claude Code's Read, Write, and Edit tools are checked against path policies.
It returns one of the following results:
| Decision | Claude Code | Codex CLI |
|---|---|---|
deny |
Rejects the command | Rejects the command |
ask |
Prompts the user for confirmation | Converts the result to deny because standalone ask is not supported |
| No decision | Delegates to the normal approval flow | Delegates to the normal approval flow |
PolicyApprovalGate never executes the operation itself. It only inspects the command string or file path, then returns a decision to the host.
What Does It Check?
The built-in rules check operations such as:
- Recursive force deletion targeting
/or the current user's home directory - Formatting a filesystem or writing directly to a block device
- Downloading content and piping it directly into a shell
- Force-pushing to or deleting protected branches
- Writing to or deleting files outside the project
- Accessing
.envfiles, SSH keys, credential files, and other sensitive paths - Writing to PolicyApprovalGate itself or to hook configuration files
PolicyApprovalGate parses shell syntax with mvdan.cc/sh rather than relying solely on simple regular expressions.
This allows it to track cases such as the following where possible:
- Relative paths after changing directories, as in
cd /tmp && rm -rf target - Symbolic links that point outside the project
- Wrappers such as
envandcommand - Pushes performed with
git -C - Commands using
cp -torinstall --target-directory - Quoted paths that contain spaces
However, PolicyApprovalGate is not a complete shell interpreter. Unsupported syntax and operations that cannot be determined statically are delegated to the host's normal approval flow by default.
Keeping Sensitive Data Out of the Agent Context
Preventing commands from modifying or deleting data is only part of the problem. It is equally important to avoid letting the agent read secrets into its context.
PolicyApprovalGate's built-in configuration treats paths such as the following as sensitive_paths:
-
.envfiles and their variants -
.sshdirectories and SSH keys -
.pem,.p12,.pfx, and.keyfiles - AWS credential files
- Authentication files such as
.netrc
By default, supported reads return ask, while writes and deletions return deny. Claude Code asks for confirmation before a read, while Codex CLI converts ask to deny.
If you do not want these files to be read even after confirmation, keep the existing patterns generated by policygate init and change sensitive_paths.policy.read to deny in ~/.policygate/config.yaml:
sensitive_paths:
policy:
- read: "ask"
+ read: "deny"
write: "deny"
delete: "deny"
After changing the setting, you can use evaluate to check the decision without reading the actual file:
policygate check-config
policygate evaluate --host claude --command 'cat ~/.ssh/id_rsa'
PolicyApprovalGate also applies the same path policies to Claude Code's direct Read, Write, and Edit tools. It does not currently inspect Grep, Glob, or every other tool call, and it cannot prevent every read performed through unsupported commands or arbitrary child processes.
To prevent Claude Code's own file tools from reading sensitive files under your home directory, configure the standard permissions.deny rules as well:
{
"permissions": {
"deny": [
"Read(~/.ssh/**)",
"Read(~/.aws/**)",
"Read(~/.gnupg/**)",
"Read(~/.config/gh/**)",
"Read(**/.env*)"
]
}
}
The Claude Code permissions documentation explains that Read deny rules apply not only to built-in file tools but also to Bash file commands that Claude Code recognizes, including cat, head, tail, and sed. They do not cover arbitrary subprocesses, such as Python or Node.js programs that access files directly. If every child process must be restricted at the OS level, combine these controls with Claude Code's sandbox filesystem restrictions, a container, or a dedicated operating-system user.
In other words, the responsibilities are divided across these layers:
- Use
permissions.denyfor host-native restrictions on Claude Code's file tools and recognized Bash file commands - Use PolicyApprovalGate's
sensitive_pathsfor an independent policy and audit layer over supported shell commands and Claude Code's Read, Write, and Edit tools - Use sandboxing or OS permissions when stronger restrictions must also cover arbitrary child processes
PolicyApprovalGate does not completely protect secrets on its own. It is an additional layer designed to work alongside existing permission controls.
Deny Rules and Confirmation Rules
PolicyApprovalGate stores its configuration in YAML.
For example, you can add an ask rule when you always want confirmation before git push:
ask:
- pattern: '(^|[;&|])\s*(/\S*/)?git\s+push\b'
reason: "Confirm before pushing to a remote"
When this rule matches in Claude Code, the PreToolUse hook returns ask.
According to the Claude Code Hooks documentation, an ask result from a PreToolUse hook forces a confirmation prompt even in Auto mode, so the classifier cannot silently approve the command. A deny result rejects the command itself.
Codex CLI does not support a standalone ask decision from PreToolUse. PolicyApprovalGate therefore converts ask to deny when invoked with --host codex, choosing the safer behavior instead of allowing the command to proceed without confirmation.
File Tools and Codex apply_patch
Claude Code passes a file path, rather than a command string, to its Read, Write, and Edit tools. PolicyApprovalGate applies sensitive_paths, protected_paths, path_scope, and its self-protection rules to those paths. It deliberately does not apply command-oriented regular expressions to file names.
Codex edits files with apply_patch. In observed Codex CLI behavior, the patch reaches the PreToolUse hook as a Bash command. PolicyApprovalGate extracts paths from *** Add File:, *** Update File:, *** Delete File:, and *** Move to: markers before applying command rules and path policies.
This is still bounded static inspection. If the target path exists only in a separate file or dynamically generated input and does not appear in the command received by the hook, PolicyApprovalGate cannot inspect it.
You can test a file-tool decision without accessing the file:
policygate evaluate --host claude --tool Read --file-path ~/.ssh/id_rsa
Install and Try It Without Running a Command
Install the latest release after reviewing the installer:
curl -fsSLO https://raw.githubusercontent.com/nobuo-miura/PolicyApprovalGate/main/install.sh
less install.sh
sh install.sh
Create the configuration file:
policygate init
policygate check-config
The evaluate command lets you check the decision without executing the command being evaluated:
policygate evaluate --host claude --command 'rm -rf /'
It returns a deny decision as JSON. For readability, the example below is formatted and omits the matched_by field included in the actual output:
{
"decision": "deny",
"reason": "Recursive force-delete of / or $HOME",
"source": "deny_rule"
}
Instead of enabling the hook immediately after changing the configuration, you can first use check-config and evaluate to verify that it produces the intended decisions.
Register It with Claude Code and Codex CLI
Use the built-in installer so the required matchers and the current binary path are registered correctly:
policygate install-hook --host claude
policygate install-hook --host codex
policygate doctor
For Claude Code, the command registers Bash, PowerShell, Read, Write, and Edit. Project-local registration uses .claude/settings.local.json; add --user to register in the user settings instead. For Codex, it updates ~/.codex/config.toml. Run /hooks in Codex and trust the displayed hook before relying on it.
How It Fits with Claude Code's Native Deny Rules
If you only use Claude Code and need to reliably block a simple command or path, the standard permissions.deny rules should be your first choice. Managed settings are also available when an organization needs to enforce these rules.
PolicyApprovalGate is intended for cases such as:
- Using the same policy with both Claude Code and Codex CLI
- Taking protected branches, working directories, and symbolic links into account
- Recording the reason for each decision in a local audit log
- Adding an independent layer alongside existing permission settings
You do not have to choose one or the other. PolicyApprovalGate is intended to be layered onto the areas where it is needed while retaining the host's standard permission controls and sandbox.
Limitations
PolicyApprovalGate is a defense-in-depth guardrail, not a security boundary.
- It covers shell commands on both hosts, including Codex
apply_patch, and Claude Code's Read, Write, and Edit tools; other tool calls remain out of scope - File tools receive path-policy checks only; command-oriented deny, ask, allow, and
unknown.actionrules are not applied to file names - An
apply_patchtarget cannot be inspected if it does not appear in the command string received by the hook - An
askdecision prompts for confirmation in Claude Code but is converted todenyin Codex, so the same policy can prompt on one host and reject the command on the other - It cannot fully handle obfuscated commands or dynamically determined command names
- It does not resolve Git aliases, so a push hidden behind an alias can bypass the protected-branch check; use remote branch protection when that rule must hold
- Unknown commands and shell syntax that cannot be parsed follow
unknown.actionandparse_error.action; both default todeferand can be changed toaskordeny. Claude Code prompts for anaskfallback, while Codex converts it todeny - An invalid explicit policy configuration returns
denyfor a valid Bash hook call; hook input, decision-output, and audit-log failures are reported to standard error but cannot always produce or replace the host decision - PowerShell input on Windows is experimental
It is not a replacement for remote branch protection, OS permissions, sandboxing, or human review.
Closing
Writing a prohibition in CLAUDE.md and mechanically enforcing a rule immediately before execution serve different purposes. A prompt communicates intent; it is not a policy enforcement mechanism.
You can use CLAUDE.md to communicate everyday development policies, while a PreToolUse hook checks hard boundaries such as “never execute this command” or “always ask before doing this.” PolicyApprovalGate brings this two-layer approach to both Claude Code and Codex CLI.
nobuo-miura
/
PolicyApprovalGate
A rule-based PreToolUse hook for Claude Code and Codex CLI that denies dangerous Bash commands, asks for confirmation on risky ones, and audits every call — no AI/LLM required.
PolicyApprovalGate
PolicyApprovalGate is a PreToolUse hook that checks shell commands against local rules before Claude Code or Codex CLI runs them. On Claude Code, it also checks direct file access through Read, Write, and Edit.
It detects dangerous commands, pushes to protected branches, and access to out-of-project or sensitive paths, then records the decision in an audit log. It does not use AI or an LLM, so the same input always produces the same result.
Important
PolicyApprovalGate complements the host permission model, sandbox, and human review. It is neither a complete shell analyzer nor a security boundary, and should not be your only line of defense.
In this example, PolicyApprovalGate blocks Claude Code from reading ~/.ssh and returns the reason to the host.
Features
- Deterministic local decisions without AI or an LLM
- Always-on protection against recursive force-deletion of the filesystem root or current user's home
- Configurable command…

Top comments (2)
The line I would promote out of the body and into Limitations is the ask conversion on Codex CLI. Same policy file, two operator experiences: a prompt on one host, a dead command on the other. A team working mostly in Codex learns that ask feels broken and starts writing everything as deny, which collapses the confirmation tier into the rejection tier and undoes the reason the gate exists. Second thing, on failing open. Delegating unclassifiable syntax to the host approval flow seems right for the ambiguous middle. Doing the same on an internal error seems less right, because the operator gets no signal that the check never ran. And since Git aliases go unresolved, someone with git p aliased to git push walks through the push rule. Is there appetite for a strict mode where internal errors and unresolvable syntax become ask rather than delegation?
Thanks — this is a helpful distinction.
I've updated the Limitations section to make the host difference more prominent: an
askdecision prompts in Claude Code but is converted todenyin Codex, so the same policy can produce two different operator experiences.I also clarified the fail-open wording. Unknown commands and unparseable syntax default to
defer, but both can already be configured asdeny. In enforce mode, if an explicitly selected policy file is unreadable or invalid, PolicyApprovalGate returnsdenyfor a valid Bash hook call instead of falling back to embedded defaults. Hook input/output and audit-log failures are reported separately.Git aliases remain an intentional limitation because resolving them requires reading Git configuration, and aliases can execute arbitrary shell commands. I've made the protected-branch bypass clearer and continue to recommend remote branch protection when that rule must hold.
A stricter fallback that uses
askis worth considering. It would require host-specific behavior:askin Claude Code anddenyin Codex. I'll treat that as a possible follow-up rather than committing to the design yet.Thanks for the thoughtful feedback!