Skip to content

Instantly share code, notes, and snippets.

@AojdevStudio
Created February 25, 2026 13:02
Show Gist options
  • Select an option

  • Save AojdevStudio/4002a6d1706a6cecdbd536d626871514 to your computer and use it in GitHub Desktop.

Select an option

Save AojdevStudio/4002a6d1706a6cecdbd536d626871514 to your computer and use it in GitHub Desktop.
Claude Code Context Monitor Hook — makes your AI agent aware of its own context window limits
#!/usr/bin/env bun
/**
* ContextMonitor.hook.ts — Agent Context Awareness (PostToolUse)
*
* Makes the agent aware of context window limits by injecting warnings
* via additionalContext when usage approaches the compaction threshold.
*
* Architecture:
* 1. statusline-command.sh writes /tmp/pai-ctx-{session_id}.json on every tick
* 2. This hook reads that bridge file on every PostToolUse
* 3. When thresholds are exceeded, injects additionalContext into the agent
*
* Thresholds (scaled to compaction at 83%):
* WARNING @ 70% raw = ~84% of compaction window
* CRITICAL @ 78% raw = ~94% of compaction window
*
* Debounce: Warns every 5 tool calls. Severity escalation bypasses debounce.
*
* TRIGGER: PostToolUse (matcher: .*)
* PERFORMANCE: <2ms. Never blocks — outputs continue immediately.
*
* Inspired by: gsd-build/get-shit-done context-monitor.js
*/
import { existsSync, readFileSync, writeFileSync } from 'fs';
const WARNING_PCT = 70;
const CRITICAL_PCT = 78;
const DEBOUNCE_CALLS = 5;
type Severity = 'warning' | 'critical';
interface BridgeData {
used_pct: number;
remaining_pct: number;
timestamp: number;
}
interface WarnState {
callsSinceWarn: number;
lastLevel: Severity | null;
}
function getSeverity(usedPct: number): Severity | null {
if (usedPct >= CRITICAL_PCT) return 'critical';
if (usedPct >= WARNING_PCT) return 'warning';
return null;
}
function getWarningMessage(severity: Severity, usedPct: number, remainingPct: number): string {
if (severity === 'critical') {
return (
`CONTEXT MONITOR CRITICAL: Usage at ${usedPct}%. Compaction imminent (~${83 - usedPct}% remaining before autocompact). ` +
`STOP new work. Do not spawn agents. Do not write long outputs. ` +
`Save algorithm state. Complete current tool call and inform the user.`
);
}
return (
`CONTEXT MONITOR WARNING: Usage at ${usedPct}%. ~${83 - usedPct}% remaining before autocompact. ` +
`Begin wrapping up current task. Avoid spawning new agents or starting complex work. ` +
`Prefer concise outputs.`
);
}
async function main() {
// Read stdin for hook input
let input: any;
try {
const reader = Bun.stdin.stream().getReader();
let raw = '';
const read = (async () => {
while (true) {
const { done, value } = await reader.read();
if (done) break;
raw += new TextDecoder().decode(value, { stream: true });
}
})();
await Promise.race([read, new Promise<void>(r => setTimeout(r, 200))]);
if (!raw.trim()) {
console.log(JSON.stringify({ continue: true }));
return;
}
input = JSON.parse(raw);
} catch {
console.log(JSON.stringify({ continue: true }));
return;
}
const sessionId = input?.session_id;
if (!sessionId) {
console.log(JSON.stringify({ continue: true }));
return;
}
// Read bridge file from statusline
const bridgePath = `/tmp/pai-ctx-${sessionId}.json`;
if (!existsSync(bridgePath)) {
console.log(JSON.stringify({ continue: true }));
return;
}
let bridge: BridgeData;
try {
bridge = JSON.parse(readFileSync(bridgePath, 'utf-8'));
} catch {
console.log(JSON.stringify({ continue: true }));
return;
}
// Stale check — if bridge is older than 60s, skip
const now = Math.floor(Date.now() / 1000);
if (now - bridge.timestamp > 60) {
console.log(JSON.stringify({ continue: true }));
return;
}
const usedPct = Math.round(bridge.used_pct);
const severity = getSeverity(usedPct);
// No threshold exceeded — silent exit
if (!severity) {
console.log(JSON.stringify({ continue: true }));
return;
}
// Debounce logic
const warnPath = `/tmp/pai-ctx-${sessionId}-warned.json`;
let warnState: WarnState = { callsSinceWarn: 0, lastLevel: null };
try {
if (existsSync(warnPath)) {
warnState = JSON.parse(readFileSync(warnPath, 'utf-8'));
}
} catch {
// Corrupted state file — reset
}
warnState.callsSinceWarn++;
const isFirstWarn = warnState.lastLevel === null;
const severityEscalated = severity === 'critical' && warnState.lastLevel === 'warning';
if (!isFirstWarn && warnState.callsSinceWarn <= DEBOUNCE_CALLS && !severityEscalated) {
// Debounced — update counter and exit silently
writeFileSync(warnPath, JSON.stringify(warnState));
console.log(JSON.stringify({ continue: true }));
return;
}
// Fire warning
warnState.callsSinceWarn = 0;
warnState.lastLevel = severity;
writeFileSync(warnPath, JSON.stringify(warnState));
const message = getWarningMessage(severity, usedPct, bridge.remaining_pct);
console.log(JSON.stringify({
continue: true,
hookSpecificOutput: {
hookEventName: 'PostToolUse',
additionalContext: message,
},
}));
process.stderr.write(`[ContextMonitor] ${severity.toUpperCase()}: ${usedPct}% used\n`);
}
main().catch(() => {
// Ensure we always output continue even on unhandled errors
console.log(JSON.stringify({ continue: true }));
});

Claude Code Context Monitor Hook

A PostToolUse hook that makes your Claude Code agent aware of its own context window limits.

The Problem

Claude Code's status line shows you the context usage percentage, but the agent itself is completely blind to it. It will happily spawn complex parallel work at 80% capacity and then lose everything when autocompaction kicks in.

The Solution

Two pieces:

  1. Bridge file — Your status line writes context metrics to /tmp/pai-ctx-{session_id}.json every tick
  2. PostToolUse hook — Reads the bridge file after every tool call and injects warnings via additionalContext

Thresholds (tuned for autocompaction at ~83%)

Level Fires At Message
WARNING 70% used "Wrap up current task. Avoid spawning agents."
CRITICAL 78% used "STOP new work. Save state. Inform user."

Features

  • Debounce: Only warns every 5 tool calls (prevents spam)
  • Severity escalation: WARNING → CRITICAL bypasses debounce and fires immediately
  • Stale check: Ignores bridge data older than 60 seconds
  • Never blocks: Outputs {continue: true} immediately on every code path

Setup

1. Add bridge file write to your status line script

After computing context_pct and session_id, add:

# Write context bridge for ContextMonitor hook
if [ -n "$session_id" ]; then
    printf '{"used_pct":%s,"remaining_pct":%s,"timestamp":%s}' \
        "$context_pct" "$context_remaining" "$(date +%s)" \
        > "/tmp/pai-ctx-${session_id}.json" 2>/dev/null
fi

2. Save the hook

Save ContextMonitor.hook.ts somewhere in your Claude Code setup (e.g., ~/.claude/hooks/).

Make it executable: chmod +x ~/.claude/hooks/ContextMonitor.hook.ts

3. Register in settings.json

Add to your hooks.PostToolUse array:

{
  "matcher": ".*",
  "hooks": [
    {
      "type": "command",
      "command": "~/.claude/hooks/ContextMonitor.hook.ts"
    }
  ]
}

4. Customize thresholds

Edit WARNING_PCT and CRITICAL_PCT in the hook to match your compaction threshold. The defaults assume autocompaction at ~83%.

Inspired by

gsd-build/get-shit-done context monitor hook. Adapted for Bun TypeScript with PAI-specific thresholds and debounce patterns.

Requirements

  • Bun runtime (Claude Code uses Bun natively)
  • A status line that computes context usage (Claude Code provides context_window.used_percentage)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment