I built an open-source tool called PolicyApprovalGate. It uses local rules to deny commands or require confirmation immediately before Claude Code or Codex CLI runs a Bash command.
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 applies rule-based policies before Claude Code or Codex CLI runs a Bash command. It checks dangerous commands, pushes to protected branches, and access to out-of-project or sensitive paths, then records decisions in an audit log.
Important
PolicyApprovalGate complements your existing 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.
The built-in rules are intended to reduce missed, clearly dangerous operations, not to block every possible risk. By default, operations that cannot be classified are delegated to the host's normal approval flow.
Features
- Deterministic rule evaluation without AI or an LLM
- Regex-based denial of dangerous commands
- Regex-based ask rules that prompt in Claude Code and are converted to deny in Codex
- Structural, always-on denial of recursive force-deletion targeting the filesystem root or current user's…
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 every time, immediately before a Bash command 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 to run a Bash command, PolicyApprovalGate receives the tool call through standard input and checks it against the configured rules.
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 command itself. It only inspects the command string and paths, 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 Bash commands return ask for reads and deny for writes and deletions. 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'
There is an important limitation: PolicyApprovalGate only evaluates Bash tool calls. It does not inspect direct reads performed through Claude Code's Read, Grep, or Glob tools. Nor can it 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.denyto block Claude Code's built-in file tools and recognized Bash file commands - Use PolicyApprovalGate's
sensitive_pathsto confirm or deny access by supported Bash commands, providing an additional shared layer for Claude Code and Codex CLI - 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.
Try It Without Running the Command
At the time of writing, PolicyApprovalGate does not yet have a tagged release, so build it from the repository:
git clone https://github.com/nobuo-miura/PolicyApprovalGate.git
cd PolicyApprovalGate
go build -o policygate ./cmd/policygate
sudo install -m 0755 policygate /usr/local/bin/policygate
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
Add the PreToolUse hook to .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/usr/local/bin/policygate --host claude"
}
]
}
]
}
}
If you are not ready to let the hook block commands, start in observe mode and review only the decisions and audit log:
{
"type": "command",
"command": "/usr/local/bin/policygate observe --host claude"
}
Register It with Codex CLI
Add the hook to ~/.codex/config.toml:
[[hooks.PreToolUse]]
matcher = "^Bash$"
[[hooks.PreToolUse.hooks]]
type = "command"
command = "/usr/local/bin/policygate --host codex"
After registering it, run /hooks in Codex, review the displayed hook, and trust it. Hooks that have not been trusted, or that have changed since they were trusted, are not executed.
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 only covers Bash tool calls
- It does not inspect direct file edits or other tools
- It cannot fully handle obfuscated commands or dynamically determined command names
- It does not resolve Git aliases
- It fails open to the host's normal approval flow when an internal error occurs
- 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.
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 applies rule-based policies before Claude Code or Codex CLI runs a Bash command. It checks dangerous commands, pushes to protected branches, and access to out-of-project or sensitive paths, then records decisions in an audit log.
Important
PolicyApprovalGate complements your existing 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.
The built-in rules are intended to reduce missed, clearly dangerous operations, not to block every possible risk. By default, operations that cannot be classified are delegated to the host's normal approval flow.
Features
- Deterministic rule evaluation without AI or an LLM
- Regex-based denial of dangerous commands
- Regex-based ask rules that prompt in Claude Code and are converted to deny in Codex
- Structural, always-on denial of recursive force-deletion targeting the filesystem root or current user's…
Top comments (1)
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?