Skip to content

Permission Flow

Marty McEnroe edited this page Feb 15, 2026 · 2 revisions

Permission Flow

This page documents how unleashed detects and auto-approves Claude Code permission prompts — from raw PTY bytes to carriage return.

The Permission Lifecycle

sequenceDiagram
    participant CC as Claude Code
    participant PTY as PTY (pywinpty)
    participant Reader as PTY Reader (t2)
    participant Sentinel as Worker Thread
    participant Term as Terminal

    CC->>PTY: Render permission prompt (Ink)
    PTY->>Reader: Raw bytes (ANSI + text)
    Reader->>Term: Pass through to display

    Reader->>Reader: Search PERMISSION_PATTERNS
    Note over Reader: Pattern found in overlap + current chunk

    Reader->>Reader: extract_permission_context_structured()
    Note over Reader: Returns (tool_type, tool_args, context)

    alt Shadow Mode
        Reader->>Reader: Write to shadow log
    end

    alt Sentinel Enabled + Tool in Scope
        Reader->>Reader: Set in_approval = True
        Reader->>Sentinel: Spawn worker thread
        Reader->>Reader: Return (resume reading)

        Sentinel->>Sentinel: LocalRules.check_bash()
        alt Local ALLOW/BLOCK
            Note over Sentinel: Resolved in <1ms
        else UNCERTAIN
            Sentinel->>Sentinel: Haiku API call (1-3s)
        end

        alt ALLOW
            Sentinel->>PTY: pty.write('\r')
        else BLOCK
            Sentinel->>Term: Red warning to stderr
            Note over Sentinel: CR withheld — user decides
        else ERROR
            Sentinel->>Term: Yellow warning to stderr
            Sentinel->>PTY: pty.write('\r') (fail-open)
        end

        Sentinel->>Sentinel: Set in_approval = False
    else No Sentinel or Tool Not in Scope
        Reader->>Reader: Set in_approval = True
        Reader->>Reader: sleep(0.1)
        Reader->>PTY: pty.write('\r')
        Reader->>Reader: Set in_approval = False
    end
Loading

Permission Detection

The 4 Patterns

Claude Code renders permission prompts with these text strings:

PERMISSION_PATTERNS = [
    b'Tab to amend',
    b'Do you want to proceed?',
    b'Allow this command to run?',
    b'Do you want to allow Claude to fetch this content?',
]

These are matched as byte patterns against the ANSI-stripped search chunk. The search includes a 256-byte overlap buffer from the previous read to catch patterns that span two pty.read(8192) chunks.

Why ANSI-Stripped Matching

The patterns are searched in ANSI-stripped text (via strip_ansi()), not raw bytes:

  1. Ink's cursor-positioning codes intersperse ANSI sequences within text — raw byte matching would fail on fragmented rendering
  2. strip_ansi() replaces ANSI codes with spaces and collapses runs, preserving word boundaries
  3. The overlap buffer + stripping together ensure reliable pattern detection

The Overlap Buffer

# Prevents missing patterns split across reads
search_chunk = self.overlap_buffer + raw_bytes
clean_chunk = strip_ansi(search_chunk)
# ...
self.overlap_buffer = raw_bytes[-256:]

The longest pattern (b'Do you want to allow Claude to fetch this content?') is 50 bytes. A 256-byte overlap guarantees detection even if the pattern spans a read boundary.

Tool Type Extraction

Once a permission is detected, extract_permission_context_structured() identifies what tool triggered it:

TOOL_CALL_RE = re.compile(
    r'(Read|Write|Edit|Bash|Glob|Grep|WebFetch|WebSearch|Skill|Task|NotebookEdit)\(([^)]{1,500})\)',
    re.DOTALL
)

This regex runs against the ANSI-stripped context buffer (last ~2KB of output). It returns:

  • tool_type: "Bash", "Write", "Edit", etc.
  • tool_args: The content inside parentheses (command text for Bash, file path for Write/Edit)
  • raw_context: Fallback string if no regex match

Known issue: The 2KB context buffer may be too small for tool calls preceded by large output (#43).

Timing Analysis

Phase Duration Notes
Pattern detection <1ms Byte string search in ~8KB chunk
Tool extraction <1ms Regex on 2KB context buffer
Shadow log write <1ms Append to file, flush
Instant approval ~200ms 100ms sleep + CR + 100ms sleep
Sentinel local rules <1ms Regex matches against safe/block patterns
Sentinel API call 800ms-3s Haiku API (p50 ~800ms, p99 ~2.5s, timeout 3s)
Total (no sentinel) ~200ms Imperceptible to user
Total (sentinel, local hit) ~200ms Same as no sentinel
Total (sentinel, API call) 1-3s Noticeable but acceptable

Edge Cases

The in_approval Flag

The boolean in_approval flag prevents:

  • Double-approval: Two CRs sent for one permission prompt
  • Overlapping sentinel checks: Only one worker thread at a time
  • Permission detection during approval: Reader skips pattern matching while flag is True

Known issue: No timeout on the flag. If the worker thread hangs, all subsequent approvals are suppressed (#41).

Rapid-Fire Permissions

Claude Code sometimes emits multiple permission prompts in quick succession (e.g., when spawning sub-agents). The in_approval flag serializes these — the second prompt waits until the first is resolved.

Pattern Spoofing

If Claude's LLM output contains one of the four pattern strings (e.g., in a code comment or documentation), unleashed could detect a false permission prompt. See #39 for analysis and mitigation.

ANSI Fragmentation

Ink sometimes renders text with escape sequences between characters: \x1b[36mA\x1b[0mllow. The byte-level pattern match still succeeds because the pattern spans enough of the visible text. However, heavily fragmented rendering (one escape sequence per character) could break detection.

Sentinel Scope Configuration

The --sentinel-scope flag controls which tool types route through sentinel:

Scope Tool Types Evaluated Use Case
bash Bash only Default — most dangerous tool type
write Bash, Write, Edit File modification protection
all Bash, Write, Edit, WebFetch, WebSearch, Skill, Task Full audit mode

Tools not in scope are auto-approved instantly (no sentinel overhead).

Shadow Mode

--sentinel-shadow logs what sentinel would see without actually evaluating anything. Every detected permission writes to logs/sentinel-shadow-{session}.log:

--- [14:23:07] ---
Tool: Bash
Args: git push origin main
Context: Bash(git push origin main)

Use shadow mode to validate sentinel's tool type detection before enabling actual evaluation.

Clone this wiki locally