Claude Code and Codex Prefix Permissions Are Broken — One Hook Script Fixes Both
Prefix-based permission matching is the default safety mechanism in two of the most widely used AI coding agents, and it has a structural blind spot that any compound command can exploit. A single hook script that runs before every Bash command closes this gap across both tools without waiting for vendor fixes, and the approach generalizes to any tool that supports PreToolUse hooks.
Claude Code and Codex both enforce permissions through prefix matching — `Bash(git push:*)` only triggers when the command literally starts with `git push`. That means `cd /other/repo && git push`, `git -C /other/repo push`, or `X=1 sudo fdisk -l` all sail through because the first token is something harmless. The same gap exists for `rm`, `mkfs`, pipes to shell, and reading sensitive files.
A single PreToolUse hook script, written in Bun and TypeScript for sub-15ms cold starts, intercepts every Bash command before execution. It strips quoted data so `grep "reboot|halt"` isn't falsely blocked, identifies the real command position past environment variables and `sudo`, and returns `allow`, `deny`, or `ask` based on the actual operation and its targets. The same script works for both Claude Code and Codex, with runtime differences handled: Codex's `ask` is documented but rejected at runtime, so dangerous operations escalate to `deny` there.
An optional AST mode using tree-sitter-bash handles edge cases the regex version can't — escaped pipes, heredocs — and falls back to regex when the parser is unavailable. The full implementation, including post-write formatting hooks and per-tool configuration, is published as open-source dotfiles.
Prefix matching is not a minor implementation detail — it is the entire permission model, and it fails on any command that doesn't put the dangerous verb first, which is trivially easy to construct
Claude Code's special-case detection for `rm` in compound commands proves the engineering team knows this is a problem, but chose not to generalize the fix, leaving `git push` and `mkfs` unprotected
Codex shipping an `ask` value in its schema that the runtime rejects is a documentation-code mismatch that has persisted across multiple releases — anyone relying on the docs for security configuration gets a false sense of safety
The decision to check `rm` targets rather than block all `rm` is the right architectural instinct: permission rules should evaluate intent from arguments, not just command names, and this principle extends naturally to `git`, `systemctl`, and `env`
Using equal-length spaces to preserve index alignment between the sanitized and original command strings is a clever trick that makes hit reporting precise without a full parser
Bun's 14ms cold start versus Python's 50–100ms matters because the hook runs on every single command — the latency is user-facing and cumulative across a session