[FEATURE] Per-Chunk Streaming Response Filtering in AfterModel Hook

Resolved 💬 2 comments Opened Jan 28, 2026 by coygeek Closed Feb 28, 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

Claude Code streams responses to provide real-time feedback to users. However, there's currently no way to filter or process this stream as it arrives. The only option is to wait until the entire response is complete.

This creates problems for:

  1. Real-time content filtering: Cannot redact sensitive information (API keys, PII, internal URLs) before it's displayed to the user
  2. Live response monitoring: Cannot analyze response quality or detect issues as they stream
  3. Dynamic intervention: Cannot stop a response mid-stream if it's going in a wrong direction
  4. Compliance logging: Cannot log responses incrementally for audit trails
  5. Custom UI updates: Cannot trigger UI events based on specific content patterns as they appear

Proposed Solution

Extend the AfterModel hook (see related feature request) to support per-chunk processing during streaming.

How It Works

When AfterModel is configured with streaming: true, it fires for each chunk of the response:

{
  "hooks": {
    "AfterModel": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/stream-filter.py",
            "streaming": true
          }
        ]
      }
    ]
  }
}

Chunk Input Schema

{
  "session_id": "abc123",
  "hook_event_name": "AfterModel",
  "streaming": true,
  "chunk_index": 5,
  "is_final": false,
  "chunk": {
    "content": "Here's how to implement...",
    "delta_tokens": 12
  },
  "accumulated": {
    "content": "Full response so far...",
    "total_tokens": 156
  }
}

Chunk Output Schema

{
  "hookSpecificOutput": {
    "hookEventName": "AfterModel",
    "filteredChunk": "Here's how to implement...",
    "continue": true
  }
}

Output Options

| Field | Type | Description |
|-------|------|-------------|
| filteredChunk | string | Modified chunk content (replaces original) |
| continue | boolean | false to stop streaming and force response end |
| reason | string | Explanation if stopping early |
| suppress | boolean | true to skip this chunk entirely |

Non-Streaming Mode (Default)

When streaming: false (or omitted), AfterModel fires once with the complete response:

{
  "session_id": "abc123",
  "hook_event_name": "AfterModel",
  "streaming": false,
  "llm_response": {
    "content": "Complete response...",
    "stop_reason": "end_turn",
    "usage": {...}
  }
}

Alternative Solutions

  1. Post-response filtering: Wait for complete response, then filter. This doesn't help with real-time display and the user may already have seen sensitive content.
  1. Proxy-based filtering: Route through a proxy that intercepts the stream. This requires significant infrastructure and adds latency.
  1. UI-level redaction: Filter in the terminal UI after receiving chunks. This still processes and potentially logs sensitive content.

Priority

Medium - Would be very helpful

Important for compliance-sensitive environments but not blocking for most users.

Feature Category

Configuration and settings

Use Case Example

Scenario: Real-time PII redaction

  1. Healthcare organization uses Claude Code for code review
  2. AfterModel streaming hook scans each chunk for PII patterns
  3. Detects and redacts patient identifiers, SSNs, or medical record numbers
  4. User sees [REDACTED] instead of sensitive data
  5. Audit log records that redaction occurred (without storing the sensitive data)
#!/usr/bin/env python3
import json
import sys
import re

input_data = json.load(sys.stdin)
chunk = input_data.get("chunk", {}).get("content", "")

# PII patterns
patterns = [
    (r'\b\d{3}-\d{2}-\d{4}\b', '[SSN REDACTED]'),  # SSN
    (r'\b[A-Z]{2}\d{6}\b', '[MRN REDACTED]'),       # Medical record
]

filtered = chunk
for pattern, replacement in patterns:
    filtered = re.sub(pattern, replacement, filtered)

output = {
    "hookSpecificOutput": {
        "hookEventName": "AfterModel",
        "filteredChunk": filtered
    }
}
print(json.dumps(output))

Scenario: Response quality monitoring

  1. Team wants to detect when Claude starts producing low-quality output
  2. AfterModel streaming hook analyzes each chunk
  3. Detects repetition, nonsense, or off-topic content
  4. Sets continue: false with reason "Response quality degradation detected"
  5. Saves API costs by stopping bad responses early

Scenario: Secret detection

  1. Security policy requires no secrets in Claude output
  2. AfterModel streaming hook runs secret detection on each chunk
  3. If API key pattern detected, redacts it immediately
  4. Logs security event for review
  5. User never sees the exposed secret

Scenario: Custom progress indicators

  1. Developer wants rich progress feedback during long responses
  2. AfterModel streaming hook detects section headers in markdown
  3. Sends updates to a status bar: "Claude is writing: ## Implementation Details"
  4. Provides better UX for long-running generations

Additional Context

Prior Art

Gemini CLI's AfterModel hook fires per-chunk during streaming:

"AfterModel (post-response chunks): Input: llm_request, llm_response. Control: Redact/filter chunks, block turn. Fires per-chunk during streaming."

This demonstrates the feature is both implementable and valuable.

Implementation Considerations

  • Latency: Per-chunk hooks must be fast to avoid degrading streaming UX. Consider timeouts.
  • Buffering: May need to buffer chunks for context (e.g., to detect multi-chunk patterns)
  • Parallel execution: Per-chunk hooks should probably be sequential (not parallel) to maintain order
  • Error handling: A failing chunk hook shouldn't crash the entire stream
  • Tool calls: Tool call chunks may need special handling
  • Opt-in: Make streaming mode opt-in to avoid performance impact for users who don't need it

View original on GitHub ↗

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