docs: Add context usage reporter hook example
Add Example 6 showing how to create a hook that reports context/token usage after each Claude response: - Python script reads transcript_path to access conversation history - Estimates tokens using ~4 chars/token heuristic - Outputs one-line report: "Context: ~45k/200k tokens (77% remaining)" - Documents both Stop and UserPromptSubmit hook configurations - Explains limitations (estimate vs exact /context command)
This commit is contained in:
@@ -480,6 +480,191 @@ if __name__ == "__main__":
|
||||
}
|
||||
```
|
||||
|
||||
### Example 6: Context Usage Reporter (Stop Hook)
|
||||
|
||||
This example shows how to create a hook that reports context/token usage after each Claude response. It reads the conversation transcript and estimates token usage.
|
||||
|
||||
**How it works:**
|
||||
|
||||
1. The hook receives `transcript_path` in the JSON input - this points to a JSONL file containing all conversation messages
|
||||
2. The script reads the transcript file and calculates total character count
|
||||
3. It estimates tokens using a simple heuristic (~4 characters per token)
|
||||
4. Outputs a one-line report showing estimated usage vs model capacity
|
||||
|
||||
**File:** `.claude/hooks/context-usage.py`
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Context Usage Reporter Hook
|
||||
|
||||
Reports estimated context/token usage after each Claude response.
|
||||
Uses the transcript_path field to read conversation history and estimate tokens.
|
||||
|
||||
Limitations:
|
||||
- Token count is an ESTIMATE (~4 chars/token average)
|
||||
- Actual token usage depends on the tokenizer and includes system prompts
|
||||
- Use /context command for accurate real-time usage
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Model context limits (adjust based on your model)
|
||||
MODEL_LIMITS = {
|
||||
"default": 200000, # Claude Opus 4.5 / Sonnet
|
||||
"haiku": 200000,
|
||||
}
|
||||
|
||||
def estimate_tokens(text: str) -> int:
|
||||
"""Estimate token count from text. ~4 characters per token on average."""
|
||||
return len(text) // 4
|
||||
|
||||
def read_transcript(transcript_path: str) -> list:
|
||||
"""Read JSONL transcript file and return list of messages."""
|
||||
messages = []
|
||||
if not os.path.exists(transcript_path):
|
||||
return messages
|
||||
|
||||
with open(transcript_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line:
|
||||
try:
|
||||
messages.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return messages
|
||||
|
||||
def calculate_usage(messages: list) -> tuple[int, int]:
|
||||
"""Calculate total characters and estimated tokens from messages."""
|
||||
total_chars = 0
|
||||
|
||||
for msg in messages:
|
||||
# Handle different message formats in transcript
|
||||
if isinstance(msg, dict):
|
||||
# Check common content fields
|
||||
content = msg.get('content', '')
|
||||
if isinstance(content, str):
|
||||
total_chars += len(content)
|
||||
elif isinstance(content, list):
|
||||
# Handle content blocks (text, tool_use, etc.)
|
||||
for block in content:
|
||||
if isinstance(block, dict):
|
||||
text = block.get('text', '') or block.get('content', '')
|
||||
total_chars += len(str(text))
|
||||
elif isinstance(block, str):
|
||||
total_chars += len(block)
|
||||
|
||||
# Also count tool inputs/outputs
|
||||
tool_input = msg.get('tool_input', {})
|
||||
if tool_input:
|
||||
total_chars += len(json.dumps(tool_input))
|
||||
|
||||
estimated_tokens = estimate_tokens(str(total_chars))
|
||||
return total_chars, estimated_tokens
|
||||
|
||||
def main():
|
||||
# Read hook input from stdin
|
||||
input_data = json.load(sys.stdin)
|
||||
|
||||
# Get transcript path from hook input
|
||||
transcript_path = input_data.get('transcript_path', '')
|
||||
|
||||
if not transcript_path:
|
||||
# No transcript available, exit silently
|
||||
sys.exit(0)
|
||||
|
||||
# Read and analyze transcript
|
||||
messages = read_transcript(transcript_path)
|
||||
total_chars, estimated_tokens = calculate_usage(messages)
|
||||
|
||||
# Get model limit (default to 200k)
|
||||
max_tokens = MODEL_LIMITS.get("default", 200000)
|
||||
|
||||
# Calculate percentages
|
||||
used_percent = (estimated_tokens / max_tokens) * 100
|
||||
remaining_tokens = max_tokens - estimated_tokens
|
||||
remaining_percent = 100 - used_percent
|
||||
|
||||
# Format the report (output as systemMessage so it appears in UI)
|
||||
report = f"Context: ~{estimated_tokens:,}/{max_tokens:,} tokens ({remaining_percent:.1f}% remaining)"
|
||||
|
||||
# Output JSON with systemMessage to show in Claude Code UI
|
||||
output = {
|
||||
"systemMessage": report
|
||||
}
|
||||
print(json.dumps(output))
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/context-usage.py\"",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Sample Output:**
|
||||
|
||||
After each Claude response, you'll see a message like:
|
||||
```
|
||||
Context: ~45,230/200,000 tokens (77.4% remaining)
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
|
||||
| Aspect | Details |
|
||||
|--------|---------|
|
||||
| **Event** | `Stop` - runs after Claude finishes responding |
|
||||
| **Input** | Uses `transcript_path` field to access conversation history |
|
||||
| **Estimation** | ~4 characters per token (rough heuristic) |
|
||||
| **Output** | `systemMessage` field displays in Claude Code UI |
|
||||
| **Accuracy** | Estimate only - use `/context` for exact counts |
|
||||
|
||||
**Why use Stop hook instead of UserPromptSubmit?**
|
||||
|
||||
- `Stop` runs after Claude responds, giving a more complete picture
|
||||
- `UserPromptSubmit` runs before Claude processes, missing the response
|
||||
- Both work, but `Stop` shows total usage including Claude's response
|
||||
|
||||
**Alternative: UserPromptSubmit for Pre-Response Check**
|
||||
|
||||
If you want to check context BEFORE Claude processes your prompt:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/context-usage.py\""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## MCP Tool Hooks
|
||||
|
||||
MCP tools follow the pattern `mcp__<server>__<tool>`:
|
||||
|
||||
18
openspec/changes/add-context-usage-hook-example/proposal.md
Normal file
18
openspec/changes/add-context-usage-hook-example/proposal.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Change: Add Context Usage Reporting Hook Example
|
||||
|
||||
## Why
|
||||
|
||||
Users want to monitor their context window usage during Claude Code sessions. The hooks documentation currently lacks a practical example showing how to create a hook that reports context/token usage after each user request. This is valuable for understanding when context is getting full and when auto-compaction might occur.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add a detailed example hook that reports context usage after each user prompt
|
||||
- The hook will read the transcript file and estimate token usage
|
||||
- Include step-by-step explanation of how the hook works
|
||||
- Document the limitations (estimation vs exact token counts)
|
||||
|
||||
## Impact
|
||||
|
||||
- **Affected specs**: hooks-documentation (add new example)
|
||||
- **Affected code**: `06-hooks/README.md` (add new example section)
|
||||
- **User impact**: Users gain a practical example for monitoring context usage
|
||||
@@ -0,0 +1,18 @@
|
||||
# Hooks Documentation Specification
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Context Usage Reporting Hook Example
|
||||
The hooks lesson SHALL include a detailed example showing how to create a hook that reports context/token usage after each user request.
|
||||
|
||||
#### Scenario: User learns to create context monitoring hook
|
||||
- **WHEN** a user reads the context usage reporter example
|
||||
- **THEN** they find a complete Python script that reads the transcript file
|
||||
- **AND** they understand how to estimate token usage from conversation history
|
||||
- **AND** they see the configuration for UserPromptSubmit or Stop hooks
|
||||
- **AND** they understand the limitations of token estimation
|
||||
|
||||
#### Scenario: Hook output format is documented
|
||||
- **WHEN** a user implements the context usage hook
|
||||
- **THEN** they can generate a one-line report showing used tokens and remaining capacity
|
||||
- **AND** they understand the report is an estimate based on transcript analysis
|
||||
9
openspec/changes/add-context-usage-hook-example/tasks.md
Normal file
9
openspec/changes/add-context-usage-hook-example/tasks.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Tasks: Add Context Usage Reporting Hook Example
|
||||
|
||||
## 1. Documentation Update
|
||||
- [x] 1.1 Add new example section "Context Usage Reporter" to 06-hooks/README.md
|
||||
- [x] 1.2 Write Python hook script that reads transcript and estimates tokens
|
||||
- [x] 1.3 Add configuration example for UserPromptSubmit hook
|
||||
- [x] 1.4 Document how transcript_path provides access to conversation history
|
||||
- [x] 1.5 Explain token estimation approach and limitations
|
||||
- [x] 1.6 Show sample output format for the one-line report
|
||||
Reference in New Issue
Block a user