Hook Output Mechanisms: Context Cost Analysis¶
Report Date: 2025-10-25 Author: Claude Code Analysis Scope: Claude Code plugin hook output fields and token costs
1. Context Cost Analysis¶
feedback Field¶
- Token Cost: 0 tokens ✅
- Added to Claude's Context: NO
- Shown to User: YES (in UI)
- Visible in Transcript (Ctrl-R): YES (unless
suppressOutput: true)
Usage: Display information to users without consuming context tokens.
hookSpecificOutput.additionalContext Field¶
- Token Cost: Full text length (~4 chars per token) ⚠️
- Added to Claude's Context: YES
- Shown to User: NO (intended), YES (bug #9455)
- Visible in Transcript (Ctrl-R): Depends on
suppressOutput
Usage: Add persistent knowledge Claude needs across the session.
suppressOutput Field¶
- Token Cost: 0 tokens ✅
- Added to Claude's Context: NO
- Shown to User: Controlled by this flag
- Visible in Transcript (Ctrl-R): NO when
true
Usage: Hide hook output from transcript mode (Ctrl-R view).
continue Field¶
- Token Cost: 0 tokens ✅
- Purpose: Control whether Claude continues processing
- Values:
true(continue),false(stop with optionalstopReason)
2. Zero-Context Pattern¶
Objective: Show Contextune commands in UI without consuming tokens.
{
"continue": true,
"feedback": "💡 Contextune Commands:\n /contextune:config - Configure detection\n /contextune:stats - View statistics\n /contextune:verify - Verify detection\n\nOr type naturally - I'll detect your intent!",
"suppressOutput": false
}
Results: - Token cost: 0 tokens - User sees: Full command list with descriptions - Claude sees: Nothing (no context added) - Transcript visibility: YES (useful for user reference)
When to Use: - Command lists at session start - UI notifications and hints - Status messages - Non-critical information
3. Implementation Guide¶
SessionStart Hook: Zero-Context Command List¶
File: /Users/shakes/DevProjects/contextune/hooks/session_start.js
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
function getContextuneCommands() {
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || path.join(__dirname, '..');
const commandsDir = path.join(pluginRoot, 'commands');
if (!fs.existsSync(commandsDir)) {
return [];
}
const commands = [];
const files = fs.readdirSync(commandsDir);
for (const file of files) {
if (file.endsWith('.md')) {
try {
const content = fs.readFileSync(path.join(commandsDir, file), 'utf8');
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (frontmatterMatch) {
const frontmatter = frontmatterMatch[1];
const nameMatch = frontmatter.match(/name:\s*(.+)/);
const descMatch = frontmatter.match(/description:\s*(.+)/);
if (nameMatch) {
commands.push({
name: `/${nameMatch[1].trim()}`,
description: descMatch ? descMatch[1].trim() : ''
});
}
}
} catch (err) {
// Ignore parsing errors
}
}
}
return commands;
}
function formatCommandList(commands) {
if (commands.length === 0) {
return '💡 Contextune is ready! Type naturally and I\'ll detect commands.';
}
const lines = ['💡 Contextune Commands Available:', ''];
for (const cmd of commands) {
lines.push(` ${cmd.name}`);
if (cmd.description) {
lines.push(` ${cmd.description}`);
}
}
lines.push('', 'Or just type naturally - I\'ll detect your intent!');
return lines.join('\n');
}
function main() {
try {
const commands = getContextuneCommands();
const message = formatCommandList(commands);
// ZERO-CONTEXT PATTERN: UI message only, no token cost
const output = {
continue: true,
feedback: message,
suppressOutput: false
};
console.log(JSON.stringify(output));
process.exit(0);
} catch (err) {
// Fail silently - don't block session start
console.error('SessionStart hook error:', err.message);
process.exit(0);
}
}
if (require.main === module) {
main();
}
Hook Registration¶
File: /Users/shakes/DevProjects/contextune/hooks/hooks.json
{
"hooks": {
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/session_start.js",
"timeout": 1000,
"description": "Show Contextune commands (zero context cost)"
}
]
}
],
"UserPromptSubmit": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "uv run ${CLAUDE_PLUGIN_ROOT}/hooks/user_prompt_submit.py",
"timeout": 5000,
"description": "Contextune intent detection"
}
]
}
]
}
}
Performance Characteristics¶
| Hook Type | Pattern | Token Cost | Latency | Use Case |
|---|---|---|---|---|
| SessionStart | Zero-context | 0 tokens | <1ms | Command list UI |
| SessionStart | Context injection | 100-500 tokens | <1ms | Config/state |
| UserPromptSubmit | Context injection | ~20 tokens/match | 2-50ms | Detected commands |
4. Multi-Hook Coordination¶
Sequential Execution Order¶
{
"hooks": {
"SessionStart": [{
"hooks": [
{"command": "hook1.js"}, // Runs first
{"command": "hook2.js"} // Runs second
]
}]
}
}
Context Combination Rules¶
- Multiple
additionalContext: Concatenated in execution order - Multiple
feedback: Implementation-dependent (possible bug) - Each hook: Independent
suppressOutputcontrol
Best Practice: - ONE hook for context injection (persistent knowledge) - SEPARATE hook for UI feedback (zero-context)
5. Token Cost Examples¶
Example 1: Minimal Command List (0 tokens)¶
{
"continue": true,
"feedback": "💡 Contextune: 3 commands available. Type naturally!",
"suppressOutput": false
}
Example 2: Detailed Command List (0 tokens)¶
{
"continue": true,
"feedback": "💡 Contextune Commands:\n /contextune:config - Configure detection settings\n /contextune:stats - View usage statistics\n /contextune:verify - Verify detected command\n\nOr type naturally - I'll detect your intent!",
"suppressOutput": false
}
Example 3: Context Injection (~100 tokens)¶
{
"continue": true,
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "[Contextune Configuration]\n- Custom patterns: 5 commands\n- Detection tiers: keyword, Model2Vec, semantic\n- Min confidence: 0.7\n- Fallback: prompt user if < 0.5"
},
"feedback": "💡 Contextune loaded with custom config",
"suppressOutput": true
}
Example 4: Both Patterns (~100 tokens)¶
{
"continue": true,
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "[Contextune: 10 custom patterns loaded]"
},
"feedback": "💡 Contextune ready with your custom patterns!\n\nCommands:\n /contextune:config\n /contextune:stats\n\nType naturally for intent detection.",
"suppressOutput": false
}
6. Recommendations¶
For Contextune Plugin¶
SessionStart Hook - Use zero-context pattern:
// RECOMMENDED: Show commands without context cost
{
"continue": true,
"feedback": formatCommandList(commands),
"suppressOutput": false
}
Rationale: - Users see available commands immediately - Zero token overhead (important for long sessions) - Commands detected via UserPromptSubmit hook anyway - Claude doesn't need to "know" commands in advance
UserPromptSubmit Hook - Keep context injection:
// KEEP: Inform Claude of detected command
{
"continue": true,
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": `[Contextune detected: ${match.command}]`
},
"feedback": `💡 Suggested: ${match.command}`,
"suppressOutput": false
}
Rationale: - Claude needs to know detected command (~20 tokens) - User sees suggestion in UI (helpful feedback) - Total cost per session: 20-100 tokens (acceptable)
General Guidelines¶
Use feedback (0 tokens) for:
- ✅ Command lists and menus
- ✅ Status notifications
- ✅ UI hints and tips
- ✅ Help text and documentation
- ✅ Diagnostic information
Use additionalContext (token cost) for:
- ✅ Configuration Claude needs to remember
- ✅ Detected commands/intents
- ✅ Project state and context
- ✅ Custom rules and patterns
- ✅ Session-specific knowledge
Use suppressOutput: true for:
- ✅ Context injection (reduce UI noise)
- ✅ Internal diagnostics
- ✅ Background processing
- ✅ When feedback is redundant
7. Known Issues & Bugs¶
Issue #9455: additionalContext Shown to User¶
Expected: additionalContext should be invisible to user, only added to Claude's context
Actual: Sometimes displayed at session start
Workaround: Use suppressOutput: true to minimize visibility
Impact: Cosmetic (doesn't affect functionality)
Status: Under investigation by Anthropic
Conclusion¶
Key Findings:
feedbackhas ZERO context cost - use liberally for UIadditionalContexthas FULL token cost - use sparingly for knowledge- Zero-context pattern enables rich UI without token overhead
- Multi-hook coordination allows separation of concerns
Contextune Implementation:
- SessionStart: Zero-context command list (0 tokens)
- UserPromptSubmit: Context injection for matches (~20 tokens/match)
- Total overhead: Minimal (20-100 tokens per session)
Performance: Excellent UX with minimal token cost.
Working Examples¶
See implementation examples:
- /Users/shakes/DevProjects/contextune/examples/session_start_zero_context.js
- /Users/shakes/DevProjects/contextune/examples/session_start_with_context.js
Full analysis:
- /Users/shakes/DevProjects/contextune/docs/hook-output-analysis.md