"Report the Claude Code permission state in effect: merges every settings scope into the effective allow/ask/deny set, each rule with its source and precedence, and flags allow rules auto mode drops. Report-only. Use when: 'what permissions are actually in effect', 'show me my effective permissions', 'which of my rules survive auto mode', 'is my managed policy being read', or before changing a rule whose source is unknown. A rule ignored for its shape: `audit-permission-grants`."
Recursos
3Instal·la
npx skillscat add melodic-software/claude-code-plugins/plugins-claude-config-skills-audit-permission-state Instal·la des del registre de SkillsCat.
Arguments. [--scopes] [--entry-diff] [--lint] [--managed] [--block] [--oracle] [--critique]. Full form: (none) every read-only stage | [--scopes] [--entry-diff] [--lint] [--managed] [--block] narrow | [--oracle] [--critique] priced
Purpose
/permissions lists your rules and the settings file each one came from, and for "where is this rule
written" that is the answer, so use it. What it does not do is resolve the outcome: it will show you an
allow and a deny for the same tool without saying which wins, it cannot tell a scope that was empty
from one it could not read, there is no claude permissions subcommand or machine-readable export of
the merged allow/ask/deny set, and none of it exists outside a live session. This skill computes that
locally, off a live session, as line records a script can read.
Verification. Claim: no CLI surface exports the merged allow/ask/deny set. Basis:
CLI reference lists nopermissionssubcommand and
no standaloneconfigsubcommand; the two JSON surfaces it documents,claude auto-mode defaults
andclaude auto-mode config, print classifier rules, not permission rules. As of 2026-09-12.
Recheck when a release note mentions a permissions export or a/permissionsexport action.
It answers a question the siblings do not. audit-permission-grants asks whether the grants you
wrote are durable and portable; audit asks whether your config files are correct. This
skill asks what is in effect: which scopes exist on this machine, which of them this reader
could actually open, and what each one holds.
Scope boundary (route out)
- Grant portability and auto-mode durability (P1/P2/P3) →
claude-config:audit-permission-grants. - Settings-file correctness outside the permission plane (schema and unknown keys, baseline
deny/ask presence, deny-rule placement, MCP, hooks, plugin drift) →claude-config:audit. The
permission plane's own dead config, thedisableAutoModetype trap, and rules that cannot match
stay here, in Phase 4: they are read across all five scopes at once, which is this skill's asset
and not something a project-scope reader can do. - The instruction layer (CLAUDE.md, a natively read AGENTS.md, rules, auto-memory) → the
claude-memoryplugin.
Boundary, native Claude Code surfaces
Two native surfaces act on the same permission rules this skill reports, so "fix my permissions"
can mean any of the three.
fewer-permission-prompts(bundled skill). Ships with Claude Code rather than as a
marketplace plugin. It scans transcripts for common read-only Bash and MCP calls and adds a
prioritized allowlist to project.claude/settings.json. The model and the person can both
invoke it./permissions(built-in command, alias/allowed-tools). An interactive dialog to view,
add, and remove rules by scope, review recent auto mode denials, and edit classifier rules on its
Auto mode tab. It is reserved for the person to run; the model does not invoke it.- This skill (marketplace plugin). Computes the effective merged set across every scope, off a
live session, with source, precedence, auto mode drops, and dead config. It writes nothing.
Routing. When the bundled fewer-permission-prompts skill resolves in this session, prefer it
for reducing prompts by writing an allowlist; prefer this skill to see what is in effect. When the
report ends with a rule the person wants added, removed, or moved, offer it to the person: you can
run /permissions to view and edit these rules interactively. An unattended run records that
offer in its output instead of asking.
Mutation gate. fewer-permission-prompts writes project settings, and /permissions writes
whichever scope the person edits. This skill never chains into either on its own behalf.
Availability is never assumed. Bundled skills are gated by settings such asdisableBundledSkills, and both surfaces vary by version and host; this section states what to do
when they resolve, never that they are present. The four-part records live in
reference/native-surfaces.md.
Report-only, permanently
This skill writes no settings file, in any scope, under any flag. That is the contract, and it
holds including under --oracle. It is not the same as writing nothing at all: --oracle spawns a
real claude -p session, and a session rewrites ~/.claude.json and adds project, session-env,
security, subagent and backup state under your config directory. The flag prints that before it
spawns anything. Every other action writes nothing anywhere. Managed policy is read-only by
construction: those are admin-write OS locations or a claude.ai Owner role, so a plugin could not
author them even if it wanted to.
Arguments
Parse $ARGUMENTS. Flags narrow; they never widen.
- (no argument): every read-only stage, in order, through one entry point. See "Run it" below.
--scopes: surface records only, no rule inventory and no downstream stage. Use when the question
is "which scopes exist and which could you read", not "what is in them".--entry-diff: inventory, merge, and the auto-mode entry diff (Phase 3).--lint: inventory and the permission-plane lint (Phase 4).--managed: inventory and the managed-conformance report (Phase 6).--block: theautoModeblock lint (Phase 5). Reads the CLI, not the inventory, so it runs alone.--oracle: with--entry-diff, cross-check the prediction against the harness's own drop
narration. Spawns a realclaude -psession; never fires without this flag. See its cost
notice, which the run prints before anything is spawned.--critique: with--block, addclaude auto-mode critique. Slow, and it truncates or returns
nothing while still exiting 0, so its result is reported as unavailable rather than as clean.
Run it
One entry point runs each stage once and fans the inventory out, instead of walking every scope once
per pipeline:
bash "${CLAUDE_PLUGIN_ROOT}/skills/audit-permission-state/scripts/audit.sh" $ARGUMENTSThe phases below document what each stage decides and how to read it. Every stage script stays
independently invocable and composes on its own, which is what draft-auto-mode-rules andaudit-pass rely on; audit.sh is a convenience caller, never a gateway.
Read the status= token on every summary line before the counts above it. status=read means
the stage saw every scope; status=incomplete means at least one could not be opened and the counts
cover only what was read. A finding count of zero under status=incomplete is not a clean bill.
Phase 1: Discover and inventory
Stage: permission-state.sh, the deterministic spine every other stage consumes. It emits one record
per line:
<scope> <surface> <status> <path> one per settings surface
rule <scope> <surface> <kind> <rule text> one per allow/ask/deny entry
NOTE: <text> anything the operator must know| Field | Values |
|---|---|
scope |
managed, user, project, local, startdir-local |
surface |
file, dropin-dir, dropin-file:<name>, registry, plist (managed); settings elsewhere |
status |
present, absent, unreadable, invalid-json, skipped, not-applicable |
kind |
allow, ask, deny |
Phase 2: Merge into the effective set
Stage: permission-merge.sh, fed the inventory. It reports what is actually in force, each rule
carrying its provenance. It passes the records above through, then appends:
CAVEAT: <text> what bounds the claim
effective <kind> scopes=<a,b> precedence_basis=<token> <rule> one per live rule
inert <kind> scopes=<a,b> outranked_by=<kind> <rule> one per beaten entryTwo mechanics decide those records, and conflating them produces confident wrong answers:
- Rules merge across scopes rather than override, so the same rule in the same list at two scopes
has no winner. Both are live, andscopes=names every contributor. Never report one of them as
having overridden the other. - Kind is decided by evaluation order, deny then ask then allow, from any scope, in both
directions. A user-level deny blocks a project-level allow just as a project-level deny blocks a
user-level allow. Scope rank does not enter into it. This is what answers "why is my allow rule
ignored": theinertrecord names the rule that beat it. - A rule that is a bare tool name reaches every call of that tool. A whole-tool deny removes the
tool from context entirely, so every other rule naming it is inert, other denies included;EndConversationis the documented exception. A whole-tool ask prompts for every call, so no scoped
allow for that tool applies. Both print aNOTE:naming the tool.
reference/criteria.md maps every precedence_basis token to the sentence it follows from, and
states the two standing bounds the run prints.
Phase 3: What entering auto mode drops
On entering auto mode, broad allow rules that grant arbitrary code execution are silently dropped,
and restored when the session leaves auto mode again. This stage says which of yours survive.
It describes a transition most run shapes never make, so state the precondition when you report
it. Auto mode is the built-in starting mode in one of the seven documented run shapes: a Pro, Max,
or Team plan in a terminal or the VS Code extension. Every other shape, claude -p and the Agent SDK
among them, starts in Manual and never makes this transition. The run prints the full list as aDIFF-NOTE; carry it rather than presenting the diff as unconditional.reference/criteria.md §"The auto-mode entry diff" holds the dated record.
Stage: automode-entry-diff.sh, fed the merge.
DIFF-NOTE: <text> classifyAllShell state, bounds
entry-diff dropped class=<class> scopes=<a,b> <rule> dropped on entry
entry-diff suspended reason=classifyAllShell ... suspended while auto mode is active
entry-diff kept scopes=<a,b> <rule> carries over
entry-diff summary allow_before=<n> dropped=<n> suspended=<n> kept=<n>- Only allow rules change on entry. Deny and ask are evaluated before the classifier in every
mode, so they are not part of this diff. Do not report them as "surviving". - Neither label is permanent.
droppedandsuspendedboth describe what is in force while auto
mode is active; the rules are restored when the session leaves it, and nothing edits a settings
file. The two labels are kept apart because the remedies differ: adroppedrule is fixable by
narrowing that rule, whilesuspendedis a global switch no rule edit reaches. classnames the documented reason:blanket,interpreter-wildcard,package-manager-run,agent, ormonitor. The three shell shapes come fromlib/permission-patterns.sh, the
vocabularyaudit-permission-grantscheck P1 also scans with;agentandmonitorare
whole-tool classes this script tests on the tool token.Monitorallow rules joined the dropped
set upstream in v2.1.236, because Claude Code runs Monitor commands through the shell.autoMode.classifyAllShellinverts the answer wholesale. When true it suspends every Bash and
PowerShell allow rule, so narrow rules do not carry over. It is resolved only from the scopes
the classifier reads, so a project- or local-scope copy is reported inert rather than obeyed.--oracleis opt-in and priced. It spawns a realclaude -psession to corroborate the
prediction. Measured cost: your settings files are untouched, but~/.claude.jsonis rewritten and
project, session-env, security and subagent state appear under your config directory. A capture
that yields nothing is unavailable, never an empty drop set.
Phase 4: Configuration that is written but never read
The permission plane accepts things it silently ignores. This finds them across every scope at once,
before a session starts:
Stage: permission-plane-lint.sh, fed the inventory.
finding <severity> [<check>] <scope> <detail>
lint summary findings=<n> checks_run=<n> status=<read|incomplete>Eleven checks: three C2-* dead-config gates, C5-disableType, and seven C6-* rules-that-cannot-match, including a malformed Tool(content) rule and an uncompilable Read/Edit path.reference/criteria.md maps each to the sentence it follows from and lists the legitimate rule shapes
the checks are written NOT to flag.
C5-disableTypeis the one to read first.disableAutoModemust be the string"disable";
a boolean is valid JSON, is accepted, and does nothing, so the operator believes auto mode is
locked out when it is not.- The three
C2gates stay separate findings. Different scope sets, different version histories:
an operator who fixed one and saw the count drop would reasonably believe they had fixed all three. findings=0is a clean bill only understatus=read. Understatus=incompletea scope could
not be opened and was never linted; the accompanyingLINT-NOTEnames which.- Advisory: the lint always exits 0 when it ran. Exit 2 means it could not run at all, never
"nothing found".
Phase 5: The autoMode classifier block
A different surface from everything above: four natural-language sections an LLM classifier reads,
not permission rules the harness matches. Independent of the pipeline, it reads the CLI, not stdin:
bash "${CLAUDE_PLUGIN_ROOT}/skills/audit-permission-state/scripts/automode-block-lint.sh" [--critique]C4-defaults: a customized section that omits"$defaults". Customizing replaces the
built-in list rather than adding to it, so the finding names how many built-in entries are gone.C2b-contradiction: the same subject inallowand in a deny section.C3-shadowed: an entry an earlierhard_denyalready forecloses, so it can never fire.--critiquesurfacesclaude auto-mode critique, wrapped and never replaced. It owns the
semantic judgment. What this adds is honesty about it: measured across three consecutive runs on one
unchanged config, output was truncated mid-sentence twice and empty once, exiting 0 every time.
A mid-sentence cut is reported as truncated; an empty result says "critique returned nothing; run it
yourself" rather than implying your rules are clean.
This lane is optional, and its prerequisite is nobody else's problem. It needs python3 becauseclaude auto-mode config emits raw control characters inside string values. jq rejects the output
outright, and no line-oriented POSIX filter can repair it, since the offending byte is a raw line feed
inside a string. Absent python3 or claude, the lane prints a visible skip notice and exits 0; every
other stage still runs.
Exit status is never trusted here. A run that exits 0 having produced nothing is reportedstatus=unavailable with an explicit "this is NOT a clean bill". The distinction between "your block
is clean" and "the block was never read" is the whole point.
Phase 6: What managed policy actually enforces
An administrator deploys managed policy believing it is policy. Some of it is; some is not, and
nothing surfaces which. Stage: managed-conformance.sh, fed the inventory.
managed enforced deny <rule>: the strongest thing an administrator can write. No level,
command line included, can override a managed permission rule, and a tool denied at any level
cannot be allowed at another.managed loosenable rule …: the interaction that surprises people. "Managed is highest" and
"deny before ask before allow, from any scope" are both true: a lower-scope deny beats a managed
allow without ever overriding it.managed loosenable autoMode: a managedautoModesection is additive, not a policy
boundary. A developer cannot remove entries it provides, but a developer-addedallowcan
override an organizationsoft_deny. Permissions, hooks, MCP, sandbox-filesystem and
sandbox-network each got an exclusivity lock; auto mode did not.managed loosenable lockout:disableAutoModeset to anything but the string"disable".
This report never prescribes. It says what the consumer's own policy does and does not achieve;
every rule string it prints came from a file it read. It ships no security floor of its own.
Completeness is bounded on every run. Server-managed settings are fetched at sign-in and cached
at ~/.claude/remote-settings.json. The cache is user-writable and can be stale, so "managed" means
the local admin surfaces only; the cache is not folded in and is not the live policy. The live
delivery has no local path. A surface that could not be read gets its own note saying so, because an
administrator reading silence as "no policy deployed" is the failure this report exists to prevent.
The note routes that diagnosis to /status (Setting sources, and the Organization policy line for a
policy that did not load, a policy-helper failure, or a credential that is signed in but not the one
in use) and to claude doctor, which shows the same Organization policy line.
Record. Claim: /status and claude doctor carry an Organization policy line that says why
the organization's policy could not be loaded, and /status marks the credential that is not in
use. Basis: https://code.claude.com/docs/en/managed-settings#read-the-source-in-status and the/status row of https://code.claude.com/docs/en/commands, plus the managed-settings page's
statement that claude doctor's Organization policy line says where the policy loaded from or why
it did not (Claude Code v2.1.261 or later). As of: 2026-09-28. Recheck: those pages drop the
Organization policy line or stop naming /status as the place a managed source is shown.
Reading the output honestly
Interpret and report the records below; the judgment is over the consumer's configuration, never over
anything this skill produced. The status vocabulary carries the whole point of the skill, so do not
collapse it in the report:
absentmeans looked and found nothing.skippedmeans could not look. Never present askippedsurface as "no policy". Say the surface was not read and why. The script emits aNOTE:
naming the reason every time.- Every scope and every managed surface emits a record on every OS, including the ones that do
not apply here (not-applicable). A surface missing from the output is a defect in this reader,
not evidence about the machine. managedmeans the LOCAL managed surfaces. Server-managed settings are cached at~/.claude/remote-settings.json. The cache is not the live policy, and the script does not fold
it into the effective set. The failure read is the Organization policy line in/status. The
script says so on every run; carry it into the report rather than implying completeness.- An
askfinding names where the contract lives. The quote is on the auto mode config page:
content-scoped ask rules always force a prompt, even in auto mode, and the classifier cannot
auto-approve a match. v2.1.257 fixed the compound-command and subshell miss only. #42797 is
closed. #83766 is still open. Say so when reporting anaskresult, and point atpermissions.denywhere the outcome must hold regardless. Seereference/criteria.md. invalid-jsonis notabsent. A malformed settings file contributes no rules to the
inventory. A managed settings file, drop-in, MDM plist, or HKLM value that cannot be parsed
refuses startup (exit 1) and names the source, from v2.1.259. A user, project, or local file
shows a Settings Error; after continue,/statusnames the file, and an unparsable usersettings.jsonpauses the retention sweep and warns in/status. Report the parse failure, not
an empty scope, and do not describe a managed parse failure as silent non-enforcement.
Scopes
Five, and the two easy to get wrong: local resolves through worktrees to the main checkout, so
a reader anchored on the worktree root looks where the file is not; startdir-local is a
pre-v2.1.211 copy that is not a fallback, since permission rules from both files stay in effect.managed is four admin surfaces per OS, not one file, plus a remote-cache record for~/.claude/remote-settings.json that is not folded into the effective set. reference/criteria.md §Scopes has the full table
and the dated record for the pre-v2.1.211 boundary.
Four documented conditions keep the local file beside .claude/settings.json instead, and the
reader resolves all four: outside a git repository, repository root is the home directory, on Windows,
and repository root or its .git/.claude not owned by the current user. The basis line names which
applied. A fifth case is stated rather than detected, because it is a helper's behavior and not a
session property: the Agent SDK's resolveSettings() always reads from the starting directory.
A cloud session reads a different scope set, and the run says so. The operator's own user and
local settings are not read there, and only server-managed settings arrive, so a user-scope record in
a cloud session describes the container. CLAUDE_CODE_REMOTE is the documented detection and the only
entrypoint variable this reader branches on. reference/criteria.md §Scopes holds both dated records.
Prerequisites
jq, required for correctness. Absent, the script stops at the entry point withERROR: jq requiredand exit 2. Report the environment gap; do not report a clean bill.reg(Windows) anddefaults(macOS), required for an optional feature. Absent, that one
managed surface isskippedwith a visible notice and everything else still runs.
Gotchas
Failure modes that produce a confidently wrong answer, and the honest limits of what has been
verified on real hardware, are in reference/gotchas.md. Read it when a result
looks wrong, when debugging a stage by hand, or before trusting a managed surface on macOS or Linux.