Skip to content

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

@n0vakovic

Description

@n0vakovic

Preflight Checklist

  • I have searched existing requests and this feature hasn't been requested yet
  • 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/.../.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

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions