From 9c5c7c5049e6c6e53ddbe6aabd6b62464820b8e3 Mon Sep 17 00:00:00 2001 From: Luong NGUYEN Date: Wed, 24 Dec 2025 23:21:38 +0100 Subject: [PATCH] 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) --- 06-hooks/README.md | 185 ++++++++++++++++++ .../proposal.md | 18 ++ .../specs/hooks-documentation/spec.md | 18 ++ .../add-context-usage-hook-example/tasks.md | 9 + 4 files changed, 230 insertions(+) create mode 100644 openspec/changes/add-context-usage-hook-example/proposal.md create mode 100644 openspec/changes/add-context-usage-hook-example/specs/hooks-documentation/spec.md create mode 100644 openspec/changes/add-context-usage-hook-example/tasks.md diff --git a/06-hooks/README.md b/06-hooks/README.md index ff10e36..1666ad0 100644 --- a/06-hooks/README.md +++ b/06-hooks/README.md @@ -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____`: diff --git a/openspec/changes/add-context-usage-hook-example/proposal.md b/openspec/changes/add-context-usage-hook-example/proposal.md new file mode 100644 index 0000000..3950c1a --- /dev/null +++ b/openspec/changes/add-context-usage-hook-example/proposal.md @@ -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 diff --git a/openspec/changes/add-context-usage-hook-example/specs/hooks-documentation/spec.md b/openspec/changes/add-context-usage-hook-example/specs/hooks-documentation/spec.md new file mode 100644 index 0000000..1646030 --- /dev/null +++ b/openspec/changes/add-context-usage-hook-example/specs/hooks-documentation/spec.md @@ -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 diff --git a/openspec/changes/add-context-usage-hook-example/tasks.md b/openspec/changes/add-context-usage-hook-example/tasks.md new file mode 100644 index 0000000..ac40737 --- /dev/null +++ b/openspec/changes/add-context-usage-hook-example/tasks.md @@ -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