[FEATURE] Expose permission picker decisions (rule + option kind) to hooks

Open 💬 4 comments Opened May 21, 2026 by AndreTheHunter

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:
<scope>}
and is invisible to userspace tooling.

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:

  1. 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.

  1. 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.

  1. Cross-process / cross-machine sync. Multiple Claude Code

instances (synced dotfiles, parallel agent workflows, shared dev
environments) can't mirror picker approvals.

  1. Silent persistence failures. If settings.local.json is

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_kind mirrors the existing ACP optionId values

(allow_once / allow_always / reject_once / reject_always).

  • applied_rule is the final rule string the user confirmed — exactly

what flows into internal PermissionUpdate.addRules. Omitted/null
for allow_once / reject_once.

  • destination matches the existing PermissionUpdate.destination

enum (session / local_settings / project_settings /
user_settings).

  • persisted: false signals 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-tool in 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 ConfigChange and 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:

  1. Developer runs Claude Code in the team repo. Claude calls

mcp__pagerduty__list-incidents.

  1. 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__*.

  1. Today: the rule lands in the developer's gitignored

settings.local.json. No external tool knows it happened. The
team config never learns.

  1. With this feature: a PermissionDecision hook captures

{option_kind: "allow_always", applied_rule: "mcp__pagerduty__*",
tool_input: {…}, …}
. The hook posts to a team service that opens
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-tool and 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.

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗