Permission policy mediator for Claude Code, Codex, OpenCode, and Gemini CLI.
Project description
agentperm
One allow / ask / deny policy for Claude Code, Codex CLI, OpenCode, and Gemini CLI — with a shell parser that reads compound commands the way bash does.
What it does
Coding agents ask before running a shell command. Each ships its own permission config, and none of them actually parse the command — to the native matcher, cat foo | head -60 is one opaque string, so it can't tell that it's two harmless read-only commands and asks you anyway. Every time.
agentperm replaces those four separate configs with a single file every agent consults, and decides on the whole parsed command. Here is a policy and exactly what your agents do with it:
// ~/.agent-permissions.jsonc
{
"version": 1,
"permissions": {
"allow": ["Bash(cat:*)", "Bash(head:*)", "Bash(sed:*)", "Bash(git status:*)", "Read", "Grep"],
"ask": [{ "tool": "Bash", "command": ["sed"], "when": { "hasOption": ["-i", "--in-place"] },
"reason": "sed -i edits files in place" }],
"deny": ["Bash(sudo:*)", "Bash(rm -rf:*)"]
}
}
| Command the agent wants to run | Verdict | Why |
|---|---|---|
cat foo 2>&1 | head -60 |
allow | both segments allowed; 2>&1 is a safe redirect |
git status && cat README.md |
allow | && sequence, both sides allowed |
sed -i s/old/new/ x.txt |
ask | sed is allowed, but the ask rule on -i wins |
cat notes | ./deploy.sh |
ask | compound includes unrecognized segment: no rule matched './deploy.sh' |
rm -rf /tmp/* |
deny | deny by rule 'Bash(rm -rf:*)' |
These are the real verdicts agentperm check returns — the same answers every wired-up agent receives (the ask/deny rows quote the exact rationale string). The first two run with no prompt; the native matchers ask about both, because a single pipe or && defeats literal string matching.
Install
uv tool install agentperm # or: pipx install agentperm
Set up (once)
agentperm import # copy your agents' existing rules into ~/.agent-permissions.jsonc
agentperm install # wire agentperm into each agent's hooks
agentperm edit # open the policy in your editor ($VISUAL/$EDITOR; writes a default if none exists)
After install, each agent calls agentperm before running a tool. Your native settings keep working underneath as a fast path — nothing is taken away.
install writes to each agent's hook config (~/.claude/settings.json, ~/.codex/, ~/.gemini/settings.json, plus an OpenCode plugin), or merges into ~/.rulesync/hooks.json if you use Rulesync. Preview with --dry-run; force a path with --mode rulesync|direct. See the CLI reference.
Writing rules
Rules go in allow, ask, or deny. Three forms:
"Bash(git status:*)"— match a shell command by prefix (git statusand anything after it). Drop the:*for an exact match, or use glob tokens:*matches one argument,**matches zero or more (e.g.Bash(pnpm --dir * build:*))."Read","WebFetch(domain:github.com)"— match a non-shell tool by name (Read,mcp__memory__*,*). An optional specifier scopes by the tool's input fields:WebFetch(domain:github.com)matches a URL field on that host or a subdomain; any other specifier is a path glob (*within a segment,**across) on the tool's path fields (Read(/etc/**),Edit(src/*)). Bare name (or(*)) matches any input.- Object form — match on flags:
{ "tool": "Bash", "command": ["sed"], "when": { "hasOption": ["-i"] }, "reason": "..." }.
In a compound command the strictest segment decides — one unrecognized command turns the whole line into an ask, and deny always wins. Full grammar: policy reference.
Global + per-project
agentperm merges two policy files and applies deny over everything: broad defaults in your home directory, overrides per repo. Both apply at the same time.
// ~/.agent-permissions.jsonc — defaults, everywhere
{ "version": 1, "permissions": {
"allow": ["Bash(cat:*)", "Bash(git push:*)", "Read", "Grep"],
"deny": ["Bash(sudo:*)", "Bash(rm -rf:*)"]
}}
// ~/work/payments/.agent-permissions.jsonc — only inside this repo
{ "version": 1, "permissions": {
"allow": [
"Bash(pytest:*)", // this repo's own tools
"Bash(pnpm --dir * build:*)" // * = one arg token; ** = zero or more
],
"deny": [
{ "tool": "Bash", "command": ["git"],
"when": { "hasOption": ["--force", "-f", "--force-with-lease"] },
"reason": "force-push rewrites shared history" }
]
}}
Working inside ~/work/payments, an agent sees both at once:
| Command | Verdict | Source |
|---|---|---|
cat notes.md |
allow | global default |
pytest -q |
allow | project adds it |
pnpm --dir packages/web build |
allow | project glob rule |
git push |
allow | global — a normal push is still fine |
git push --force |
deny | project flag rule overrides the global allow |
So you allow a tool broadly once, and a single repo can both add its own commands and clamp down on the dangerous variants — without touching the global file.
Create or edit the project file with agentperm edit --local (it writes to your git repo root). import and install always act on the global file and your agents' global hooks.
Examples
Ready-to-crib policies in examples/:
starter.agent-permissions.jsonc— a minimal global policy to begin from: read-only shell and tools allowed,sed -iasks,sudo/rm -rfdenied.global.agent-permissions.jsonc— a fuller real-world global policy: a large allow-list of read-only AWS and CLI commands, thesed -iask rule, a deny list, and shell-redirection defaults.project.agent-permissions.jsonc— this repo's own per-project file, allowing just its dev-tooling commands on top of whatever your global policy permits.
Commands
| Command | What it does |
|---|---|
agentperm import |
Copy existing native rules into the policy file (Claude, Codex, OpenCode) |
agentperm install [--mode auto|rulesync|direct] [--dry-run] |
Wire agentperm into agent hooks |
agentperm edit [--global|--local] |
Open the policy in your editor ($VISUAL/$EDITOR), creating a default if missing. --global (default) is ~/.agent-permissions.jsonc; --local is this repo's root file |
agentperm check --agent <claude|codex|opencode|gemini> --event <name> |
Runtime decision: reads a hook payload on stdin, writes a verdict on stdout. The agent runs this, not you. |
agentperm --version |
Print the installed version |
Skip prompts in one pane (zellij)
Running agents inside zellij? The bundled WASM plugin adds a per-pane toggle: while it's on, both ask and unmatched (no-opinion) commands become allow in that pane only — deny rules still bite. See zellij-plugin/. (Under Claude Code's own --dangerously-skip-permissions, agentperm steps aside completely.)
What it doesn't do
- It doesn't sandbox. agentperm decides allow / ask / deny; it doesn't contain what runs. An approved command runs with your normal privileges.
- It doesn't replace native settings. Those keep working as a fast path; agentperm layers on top.
- It doesn't manage MCP servers. Use Rulesync or native config for that.
More
- Architecture · Policy reference · CLI reference
- Adapter notes · Troubleshooting · zellij plugin
- Contributing · Changelog
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file agentperm-0.2.1.tar.gz.
File metadata
- Download URL: agentperm-0.2.1.tar.gz
- Upload date:
- Size: 55.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.18
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
941918ade178ededca7c867eea7d85eabfa44f227d60bcca9fb5d2f9e0de53ee
|
|
| MD5 |
91f0abb26a7b5d5ab79633415a2b907a
|
|
| BLAKE2b-256 |
d6ae956937d98291ab01297e65f5f91b8ef35756a5d49a990fa3e764c31f69b1
|
File details
Details for the file agentperm-0.2.1-py3-none-any.whl.
File metadata
- Download URL: agentperm-0.2.1-py3-none-any.whl
- Upload date:
- Size: 33.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.18
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d9dee565184dd314524d40e11e12c8f20e0359f6dfb876ba4694e04d33f2def2
|
|
| MD5 |
12302b45e3a6383ba133c8f935868a81
|
|
| BLAKE2b-256 |
f3fc625842677387ced89b8c976cf0ea9abdc9f6a3de82a7fa3c7f648d0e3611
|