[FEATURE] Add permission decision logging to conversation JSONL for debugging

Resolved 💬 5 comments Opened Jan 7, 2026 by n0vakovic Closed Mar 3, 2026

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 investigating why permissions aren't working as expected, there's no visibility into the permission decision process. The conversation JSONL (~/.claude/projects/.../<session>.jsonl) captures:

  • ✅ Tool calls (tool_use blocks)
  • ✅ Tool results (tool_result blocks)
  • ❌ Permission prompts shown to user
  • ❌ User's allow/deny decision
  • ❌ Pattern matching results (which allowlist rule matched or didn't)
  • ❌ Source of permission (global settings vs skill frontmatter vs user approval)

Proposed Solution

Add a permission decision record type to JSONL:

```
{
"type": "permission",
"timestamp": "...",
"tool": "Bash",
"command": "echo \"CLAUDE-SESSION-BEACON-...\"",
"patterns_checked": [
{"source": "skill:find-session-convo", "pattern": "Bash(echo:*)", "matched": true},
{"source": "global:settings.json", "pattern": "Bash(git:*)", "matched": false}
],
"decision": "auto_approved" | "user_approved" | "user_denied",
"reason": "matched skill frontmatter allowed-tools"
}


### Alternative Solutions

here are alternative solutions:

  1. PreToolUse Hook Logger

  Create a hook that logs every permission request:

 ```
 // ~/.claude/settings.json
  {
    "hooks": {
      "PreToolUse": [{
        "matcher": ".*",
        "command": "~/.claude/scripts/log-permission.sh"
      }]
    }
  }
  #!/bin/bash
  # ~/.claude/scripts/log-permission.sh
  # Hook receives JSON on stdin with tool_name, tool_input, etc.

  cat >> ~/.claude/permission-decisions.log
  echo "---" >> ~/.claude/permission-decisions.log

Limitation: Only logs what gets to the hook, not the decision outcome.

---

  1. Permission Troubleshooter Skill

A skill that simulates pattern matching:

```
---
name: debug-permissions
description: Troubleshoot why a tool prompts for permission
---


  ## Steps

  1. Ask user for the tool call that prompted unexpectedly
  2. Load all permission sources:
     - `~/.claude/settings.json` (global)
     - `.claude/settings.json` (project)
     - `.claude/settings.local.json` (local)
     - Current skill frontmatter (if applicable)
  3. Simulate pattern matching against each source
  4. Report: which patterns were checked, why none matched

  ---
  3. Permission Audit Agent

  ```
---
  name: permission-audit
  description: Audit permission configuration across all sources
  allowed-tools: Bash(cat:*), Bash(jq:*), Read(*)
  ---

  ## Audit Checklist

  1. **Collect all permission sources:**
     - Global: `~/.claude/settings.json`
     - Project: `.claude/settings.json`
     - Local: `.claude/settings.local.json`
     
  2. **Parse and normalize patterns**

  3. **Test specific command against patterns:**
     User provides: `Bash: echo "hello"`
     
     Check each pattern:
     - `Bash(echo:*)` → MATCH ✓
     - `Bash(git:*)` → NO MATCH
     
  4. **Identify issues:**
     - Typos in patterns
     - Wrong key name (`allowed-tools` vs `allowedTools`)
     - File not being read (permissions, location)
     - Pattern syntax errors

  5. **Suggest fixes**

---

  1. Pattern Tester Script

```
#!/usr/bin/env python3
# ~/.claude/scripts/test-permission-pattern.py

import sys
import json
import re
import fnmatch
from pathlib import Path

def load_patterns():
"""Load patterns from all sources"""
patterns = []
sources = [
Path.home() / '.claude/settings.json',
Path('.claude/settings.json'),
Path('.claude/settings.local.json'),
]
for src in sources:
if src.exists():
data = json.loads(src.read_text())
for p in data.get('allowedTools', []):
patterns.append({'source': str(src), 'pattern': p})
return patterns

def match_tool(tool_name, tool_input, patterns):
"""Test if tool matches any pattern"""
# Simplified matching logic
for p in patterns:
# Parse pattern like "Bash(echo:*)"
# ... matching logic ...
pass

if __name__ == '__main__':
tool = sys.argv[1] # e.g., "Bash"
cmd = sys.argv[2] # e.g., "echo hello"
patterns = load_patterns()
# Test and report


### Priority

High - Significant impact on productivity

### Feature Category

Configuration and settings

### Use Case Example

_No response_

### Additional Context

 Real-World Scenario: Skill Author Debugging Permission Issues

  Context

  I'm a Claude Code power user building custom skills. I created /find-session-convo to locate the current session's JSONL file using a beacon technique.

  Step 1: Create the Skill

  ---
  description: Find current session's conversation file
  allowed-tools: Bash(echo:*), Bash(grep:*)
  ---

  ## Steps
  1. Generate beacon: `echo "CLAUDE-SESSION-BEACON-$(uuidgen)"`
  2. Find file: `grep -rl "BEACON-xxx" ~/.claude/projects/`

  Step 2: Test the Skill

  I run `/find-session-convo `and get prompted:

  Claude wants to run: echo "CLAUDE-SESSION-BEACON-..."

  Allow? [y/n/always]

  Expected: Auto-approved (I declared Bash(echo:*) in frontmatter)
  Actual: Prompted for permission

  Step 3: Try to Debug

  Check 1: Did I spell the frontmatter key correctly?
  allowed-tools: Bash(echo:*)  # or is it allowedTools?
  No documentation confirms which is correct.

  Check 2: Is my pattern syntax right?
  Bash(echo:*)     # prefix match?
  Bash(echo :*)    # with space?
  Bash(echo*:*)    # glob the command?
  No feedback on what was attempted.

  Check 3: Look at JSONL for clues
  python3 jsonl-extract.py session.jsonl --mode tools
  Output:
  1. Bash | echo "CLAUDE-SESSION-BEACON-..."
  Shows the tool ran, but nothing about:
  - Was permission prompted?
  - Did user approve, or was it auto-approved?
  - What patterns were checked?
  - Why didn't allowed-tools apply?

  Check 4: Compare with global settings
  grep "Bash(" ~/.claude/settings.json
  "Bash(git status:*)",
  "Bash(ls:*)",
  ...
  No Bash(echo:*) globally. But it's in my skill frontmatter—why didn't that work?

  Step 4: Dead End

  I cannot determine:
  1. Whether skill frontmatter allowed-tools is even being parsed
  2. Whether my pattern syntax is wrong
  3. Whether there's a precedence issue (global vs skill)
  4. Whether this is a bug or user error

  Step 5: What Would Help

  If JSONL captured permission decisions:

{
"type": "permission",
"tool": "Bash",
"command": "echo \"CLAUDE-SESSION-BEACON-...\"",
"sources_checked": [
{"source": "skill:find-session-convo", "key": "allowed-tools", "patterns": ["Bash(echo:*)"], "parsed": false, "reason": "unknown frontmatter key"}
],
"decision": "prompted_user",
"user_response": "allowed"
}


  Now I immediately see: "parsed": false, "reason": "unknown frontmatter key" — the key should be allowedTools not allowed-tools.

  The Gap

  Without permission logging, skill authors must:
  - Guess at syntax through trial and error
  - File bug reports without evidence
  - Wonder if it's their mistake or a bug
  - Spend 30+ minutes on what should be a 30-second diagnosis

View original on GitHub ↗

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