[FEATURE] Expose permission picker decisions (rule + option kind) to hooks
Preflight Checklist
- [x] I have searched existing requests and this feature hasn't been requested yet
- [x] This is a single feature request (not multiple features)
Problem Statement
When a user resolves Claude Code's permission picker — particularly the
"Yes, and don't ask again for: <PATTERN>" option where <PATTERN> is
editable — the resulting decision is not observable from any external
surface. The rule string the user confirmed flows only through Claude
Code's internal PermissionUpdate{addRules: [<PATTERN>], destination: and is invisible to userspace tooling.
<scope>}
What's currently exposed:
| Surface | Carries the user-confirmed rule string? |
| --- | --- |
| PermissionRequest hook input | No — only tool_name/tool_input/tool_use_id, and fires before the picker resolves |
| Notification[permission_prompt] hook input | No — only a message string |
| PostToolUse hook input | No — confirms the tool ran, not the rule that authorised it |
| ConfigChange[local_settings] input | No — changed_keys is path-level, not added/removed rule strings |
| Session transcript jsonl | No — picker prompts and decisions aren't logged |
| --permission-prompt-tool <mcp> | Yes — but only honored in claude -p / SDK mode, ignored in interactive use |
This blocks several real workflows:
- Audit logging. Recording "user X granted rule Y at time Z, in
tool-call context W". Today the only signal is a delta in
settings.local.json, which loses the who/when/tool-context.
- Team-shared allowlist files. Developers grant rules locally
(gitignored settings.local.json); there's no signal to propagate
them into a versioned reviewed shared file.
- Cross-process / cross-machine sync. Multiple Claude Code
instances (synced dotfiles, parallel agent workflows, shared dev
environments) can't mirror picker approvals.
- Silent persistence failures. If
settings.local.jsonis
unwritable for any reason — read-only mount, ENOSPC, EPERM,
bind-mount from a read-only layer, symlink the writer refuses to
follow — the picker's decision is silently lost. No event fires to
warn the user; next session prompts them again.
Proposed Solution
Add a new PermissionDecision hook event, fired after the user
resolves a picker prompt, with this stdin payload:
{
"hook_event_name": "PermissionDecision",
"tool_name": "Bash",
"tool_input": { "command": "git log --oneline -n 5" },
"tool_use_id": "toolu_01ABC...",
"option_kind": "allow_always",
"applied_rule": "Bash(git log *)",
"destination": "local_settings",
"persisted": true
}
option_kindmirrors the existing ACPoptionIdvalues
(allow_once / allow_always / reject_once / reject_always).
applied_ruleis the final rule string the user confirmed — exactly
what flows into internal PermissionUpdate.addRules. Omitted/null
for allow_once / reject_once.
destinationmatches the existingPermissionUpdate.destination
enum (session / local_settings / project_settings /
user_settings).
persisted: falsesignals that the rule was applied in memory but
the write to the destination file did not succeed — letting tooling
surface the silent-failure cases.
Hooks subscribe via the standard config shape:
{
"hooks": {
"PermissionDecision": [
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "..." }] }
]
}
}
Matcher semantics mirror PermissionRequest (filter on tool_name).
Alternative Solutions
- Augment
ConfigChange[local_settings]with a rule diff. Add e.g.
permissions_diff: { added: [...], removed: [...] } to the existing
event payload. Cheaper to ship but doesn't help when the write fails
silently — the event never fires in those cases. Also wouldn't
capture allow_once / reject_once decisions (no file write).
- Honor
--permission-prompt-toolin interactive mode. Today the
flag only takes effect in claude -p / SDK mode. Allowing it
interactively would let users replace the picker entirely with a
custom MCP tool that receives the full ToolPermissionRequest and
returns a PermissionUpdate. Solves every use case but a much
larger surface change.
- Current workarounds. Either (a) hook
ConfigChangeand diff the
file (misses non-persisted decisions, misses one-shot decisions,
loses tool-call context), or (b) maintain an external watcher
polling settings.local.json (same limitations, plus latency).
Priority
Medium - Would be very helpful
Feature Category
Configuration and settings
Use Case Example
A team wants a versioned, reviewed permissions.allow file checked in
to their repo. Developers can grant rules locally for their own
workflow, and an internal tool watches picker decisions and proposes
additions to the team file via pull request:
- Developer runs Claude Code in the team repo. Claude calls
mcp__pagerduty__list-incidents.
- The picker fires. The developer picks "Yes, and don't ask again
for:" and edits the suggested mcp__pagerduty__list-incidents to
the broader mcp__pagerduty__*.
- Today: the rule lands in the developer's gitignored
settings.local.json. No external tool knows it happened. The
team config never learns.
- With this feature: a
PermissionDecisionhook captures
{option_kind: "allow_always", applied_rule: "mcp__pagerduty__*",. The hook posts to a team service that opens
tool_input: {…}, …}
a PR to the shared allowlist for review, attaching the tool-call
context.
The same hook also serves audit logging (append to a journal),
cross-machine sync (push to a private gist or settings repo), and
silent-failure detection (persisted: false → toast/log a warning to
the user).
Additional Context
- The data this FR asks for is already constructed internally:
PermissionUpdate{addRules, destination} is a first-class type in
Claude Code's permission machinery, and the SDK's canUseTool
callback both receives and returns it. The ask is just to surface
the resolved value as a hook event.
- Backwards compatibility: adding a new hook event is non-breaking.
Existing hook configs that don't subscribe to PermissionDecision
are unaffected.
- Related but distinct:
--permission-prompt-tooland the SDK
canUseTool callback solve the replace the picker problem. This
FR is the smaller observe the picker change — keep Claude Code's
native UI as-is, just emit a signal when it resolves.
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗