Table of Contents generated with DocToc
permission-audit
Capability: substrate:sandbox
Harness: Claude Code, OpenCode
Audit + atomically edit Claude Code’s permissions.allow[] entries
in <repo>/.claude/settings.json and <repo>/.claude/settings.local.json.
Backs the --apply-permission-audit flag of
/magpie-setup verify
(check 8d), and is also directly usable as a CLI.
OpenCode. The same over-permissioning check applies to the other
harness through audit-opencode, which reads an
opencode.json permission
config instead of a Claude allow-list. OpenCode models permissions
differently — permission is a string or an object keyed by tool, and
permission.bash may map glob command patterns to allow/ask/deny
(last-matching-rule-wins, with a "*" default) — so this is a separate
classifier, but it enforces the same intent: flag configuration that
auto-approves dangerous shell execution. It reports, in JSON:
blanket-allow—permissionis the string"allow"(every tool auto-approved);bash-allow-all— the defaultpermission.bashdecision is"allow";dangerous-allow— a specific rule auto-approves a dangerous command family (git push,sudo,curl/wget,rm -rf, cloud CLIs,kubectl,docker run,ssh, interpreters/npx/uvx, …) while the default is stricter.
uv run --project tools/permission-audit permission-audit audit-opencode opencode.json
The Claude-only apply subcommand (atomic allow-list edits) has no
OpenCode counterpart yet; audit-opencode is read-only.
Prerequisites
- Runtime: Python 3.11+ run via
uv(uv sync/uv run); the tool itself is stdlib-only. - CLIs: None beyond the runtime.
- Credentials / auth: None.
- Network: None — operates entirely on local
.claude/settings.json/.claude/settings.local.jsonfiles. - Optional: dev group adds
pytest,ruff,mypyfor the test- lint suite.
Why
Adopter permission allow-lists drift over time. Two failure modes the framework’s verify check 8d surfaces:
- Forbidden wildcards accumulate — patterns like
Bash(uv run *),Bash(python3 *),Bash(npm run *),Bash(bash *),Bash(gh api *)are equivalent to allowing arbitrary code execution. Each is easy to add for a short-lived reason (“just for this session”) and easy to forget. - Narrow read-only patterns the framework’s skills invoke
constantly are missing — the
securityfamily’s Gmail and PonyMail read MCPs,Bash(vulnogram-api-record-fetch *),Bash(lychee *). Pre-allowing them removes the per-call confirmation prompt without expanding the capability surface.
The audit phase is a pure classification — given the allow list +
the adopter’s opt-in families, return forbidden[] (✗) and
missing_recommended[] (⚠). The apply phase is the only writer:
flock-guarded read → mutate → atomic rename.
Install
uv sync --project tools/permission-audit
Stdlib-only runtime; the dev group adds pytest, ruff, mypy.
CLI
permission-audit audit <settings-path> [--families security,issue]
permission-audit apply <settings-path> [--add <entry>]... [--remove <entry>]... [--create-if-missing]
permission-audit list-known
audit
Reads the settings file, classifies its permissions.allow[], and
prints JSON. Returns exit code 1 when any forbidden entry is
present (so a shell caller can gate a downstream action), 0
otherwise. The empty-family bucket ("") is always included in
the recommended check — those entries (Bash(lychee *) today)
apply to every adopter.
$ permission-audit audit .claude/settings.local.json --families security
{
"settings_path": "/repo/.claude/settings.local.json",
"file_exists": true,
"allow_count": 215,
"families": ["security"],
"forbidden": [
{
"severity": "forbidden",
"pattern": "Bash(uv run *)",
"json_pointer": ".permissions.allow[37]",
"family": null
}
],
"missing_recommended": [
{
"severity": "missing-recommended",
"pattern": "mcp__claude_ai_Gmail__list_labels",
"json_pointer": null,
"family": "security"
}
]
}
apply
Atomic add/remove. Concurrent writers (notably the sandbox-
allowlist helper writing sandbox.filesystem.*) serialize via
POSIX fcntl.flock on the target file. Unrelated keys
(extraKnownMarketplaces, hooks, permissions.deny, etc.) are
preserved verbatim.
permission-audit apply .claude/settings.local.json \
--remove 'Bash(uv run *)' \
--remove 'Bash(python3 *)' \
--add 'Bash(lychee *)' \
--add 'mcp__claude_ai_Gmail__get_thread'
Output: JSON describing what changed.
list-known
Dumps the canonical forbidden + recommended-by-family lists for diff-friendly inspection (used by the verify check 8d narrative and by adopters wanting to vendor the same convention).
Canonical lists
See src/permission_audit/audit.py.
Both lists are intentionally narrow:
- Forbidden — only the exact wildcard strings the verify check 8d doc enumerates. Same-category extensions (a wildcard not on the list but with the same capability surface) are the caller’s responsibility to flag — the framework cannot enumerate every variant.
- Recommended — every entry is verified against Claude Code’s
auto-allowed harness exclusions (
READONLY_COMMANDS,GIT_READ_ONLY_COMMANDS,GH_READ_ONLY_COMMANDS, …) so the tool does not propose entries that the harness would never prompt on anyway.
Tests
uv run --project tools/permission-audit pytest
Covers: classification, family-scoping, JSON-pointer numbering on duplicates, atomic add/remove, no-op rewrite skip, malformed JSON detection.
How /magpie-setup verify uses this
The verify check 8d narrative describes the human-facing report; this tool is the engine behind it. The skill calls
uv run --project <framework>/tools/permission-audit \
permission-audit audit <repo>/.claude/settings.local.json \
--families <comma-joined families from the lock>
for each of .claude/settings.json and .claude/settings.local.json,
folds the JSON output into the verify report, and — only on the
--apply-permission-audit flag and after the operator confirms
the exact list of changes — calls permission-audit apply for
each file.