Table of Contents generated with DocToc
Capability: substrate:sandbox
Harness: agnostic
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 — permission is the string "allow" (every tool auto-approved);bash-allow-all — the default permission.bash decision 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.
Kiro. audit-kiro reads a Kiro CLI agent config (.kiro/agents/<name>.json). Kiro models permissions as an allowlist/denylist rather than a per-command decision map (Kiro shell tool): allowedCommands / deniedCommands are \A..\z-anchored regex, deny-before-allow, with denyByDefault and autoAllowReadonly; and the shell tool in allowedTools auto-approves everything (overriding toolsSettings). Same intent, reported in JSON:
shell-allow-all — the shell tool (shell / execute_bash / * / @builtin) is in allowedTools (every shell command auto-approved);dangerous-allow — an allowedCommands regex auto-approves a dangerous family (unblocked by deniedCommands);exfil-allow — aws / web_fetch is blanket-allowed in allowedTools, or web_fetch.trusted is a blanket .*.uv run --project tools/permission-audit permission-audit audit-kiro .kiro/agents/<name>.json
Like audit-opencode, audit-kiro is read-only (no apply), and a missing shell policy is not flagged — Kiro's default is to prompt.
uv (uv sync / uv run); the tool itself is stdlib-only..claude/settings.json / .claude/settings.local.json files.pytest, ruff, mypy for the testAdopter permission allow-lists drift over time. Two failure modes the framework's verify check 8d surfaces:
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.security family'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.
uv sync --project tools/permission-audit
Stdlib-only runtime; the dev group adds pytest, ruff, mypy.
permission-audit audit-any [--dir <project-root>] [--families security,issue] 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-any (harness-neutral)Scans <project-root> (default: .) for every known settings file and audits each with the appropriate classifier — no need to know which harness is active. Detected harnesses:
| File | Harness |
|---|---|
.claude/settings.json | Claude Code |
.claude/settings.local.json | Claude Code (local override) |
opencode.json | OpenCode |
Returns JSON with one entry per detected file plus a top-level has_forbidden flag. Exit code 1 when any harness has forbidden entries, 0 otherwise.
$ permission-audit audit-any --dir /path/to/project { "dir": "/path/to/project", "detected_harnesses": ["claude-code", "opencode"], "results": [ { "harness": "claude-code", "settings_path": "/path/to/project/.claude/settings.json", "forbidden": [...], "missing_recommended": [...] }, { "harness": "opencode", "settings_path": "/path/to/project/opencode.json", "forbidden": [...] } ], "has_forbidden": true }
This is the recommended entry point when driving the audit from a harness that is not Claude Code or OpenCode — wire it as a startup check or CI step without any harness-specific plumbing.
auditReads 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" } ] }
applyAtomic 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-knownDumps 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).
See src/permission_audit/audit.py.
Both lists are intentionally narrow:
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.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.
/magpie-setup verify uses thisThe 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.