|
claude-warden is a hook system for Claude Code that intercepts every tool call before and after execution. It silences verbose commands, compresses large outputs, blocks unsafe network calls, enforces subagent budgets, and surfaces a live statusline — saving tens of thousands of tokens per session with negligible added latency.
- Install prerequisites:
jq(required). Optional:rg,fd. - Install hooks into
~/.claude/(symlink mode):./install.sh
- Choose a profile when prompted (or pass
--profile standard). - Start a new Claude Code session. Hooks run automatically.
Tip
Run ./install.sh --dry-run first to preview every change before anything touches ~/.claude/.
./install.sh --dry-runclaude-warden installs shell hooks that intercept every Claude Code tool call. Each hook enforces token-efficient patterns and blocks unsafe operations.
|
|
stat() checks.
| Dependency | Purpose | |
|---|---|---|
| Required | jq |
JSON processing |
Go 1.23+ |
Builds the warden-collector binary | |
| Recommended | rg, fd |
Faster search/find in hooks |
| Optional | python3 |
Warden-viewer web UI |
mitmdump |
API capture tool only |
curl -fsSL https://raw.githubusercontent.com/johnzfitch/claude-warden/master/install-remote.sh | bashDownloads a release tarball, verifies its SHA-256 checksum, validates contents, then runs install.sh --copy.
To pin a version:
curl -fsSL https://raw.githubusercontent.com/johnzfitch/claude-warden/master/install-remote.sh | bash -s -- v0.6.1Install from source (development)
git clone https://github.com/johnzfitch/claude-warden.git ~/dev/claude-warden
cd ~/dev/claude-warden
./install.shThe installer applies a configuration profile that sets token limits, tool permissions, and internal thresholds. Choose one during install or pass --profile:
| Profile | What it sets |
|---|---|
minimal |
Hooks only. No env or permission changes. For users who manage settings.json themselves. |
standard |
Token/output limits, OTEL monitoring, 40 safe tool permissions. Recommended. |
strict |
~40% tighter limits. Fewer pre-approved tools (19). Lower subagent budgets. |
./install.sh --profile standardProfiles live in config/profiles/. Create config/user.json (gitignored) for personal overrides:
cp config/user.json.template config/user.json
# Edit config/user.json, then re-install:
./install.sh --profile standardMerge order: defaults.json ← profile ← user.json ← existing settings.json (non-warden keys preserved).
Install modes & what install.sh does
| Mode | Command | Behavior |
|---|---|---|
| Symlink (default) | ./install.sh |
Edits to repo take effect immediately |
| Copy | ./install.sh --copy |
Files independent of repo |
| Dry run | ./install.sh --dry-run |
Preview changes, write nothing |
What install.sh does:
- Checks prerequisites (
jqrequired, warns ifrg/fdmissing). Detects platform. Backs up existing hooks andsettings.json. - Prompts for a profile (or uses
--profile). Deep-mergesdefaults.json+ profile +user.json. - Symlinks (or copies) hook scripts +
lib/+statusline.shinto~/.claude/. - Builds the Go collector and installs to
~/.local/bin/. - Generates
warden.env(thresholds). Merges env vars and permissions intosettings.json. - Validates JSON and shell syntax for every installed script.
./uninstall.shRestores your most recent settings.json backup. Removes ~/.claude/.warden/ config. Hook backups remain in ~/.claude/hooks.bak.*/.
All thresholds are configurable via config/defaults.json, profiles, or config/user.json. After editing, re-run ./install.sh to regenerate ~/.claude/.warden/warden.env.
| Threshold | Config key | Default | Strict |
|---|---|---|---|
| Output truncation | warden.truncate_bytes |
20KB | 10KB |
| Subagent read cap | warden.subagent_read_bytes |
10KB | 6KB |
| Output suppression | warden.suppress_bytes |
512KB | 256KB |
| Read file size limit | warden.read_guard_max_mb |
2MB | 1MB |
| Write max size | warden.write_max_bytes |
100KB | 50KB |
| Edit max size | warden.edit_max_bytes |
50KB | 25KB |
| Subagent call limits | warden.subagent_call_limits.* |
15–40 | 10–25 |
| Subagent byte limits | warden.subagent_byte_limits.* |
80–150KB | 50–100KB |
Token limits and tool permissions are set in the env and permissions sections of the config files and merged into settings.json during install.
- Read compression:
read-compress— subagent threshold at 300 lines, main agent at 500 lines - Binary detection:
post-tool-use— POSIXod+grepfor NUL bytes (full-stream scan)
Shell environment, token accounting, disabling guards, custom allow-list
Shell environment
Add to ~/.zshrc or ~/.bashrc:
# claude-warden env
source "$HOME/.claude/.warden/warden.env.sh"Exports OTEL, token limit, timeout, and sandbox vars from your profile into every new shell.
Token savings accounting
All hooks report savings to ~/.claude/.statusline/events.jsonl at ~3.5 bytes/token (estimated). For exact counts:
export WARDEN_TOKEN_COUNT=apiEach truncation event spawns a background call to the Anthropic token counting API (free, separate rate limits) and appends a correction event. Zero added latency.
- Requirements for API mode
ANTHROPIC_API_KEYin environment;python3withanthropicinstalled- Custom Python path
- Set
WARDEN_PYTHON=/path/to/venv/bin/python3if needed - Graceful degradation
- Missing key, missing package, or network failure → silently exits, estimate stands
Disabling specific guards
Remove the corresponding matcher from settings.hooks.json and re-run ./install.sh. Example — disable read compression:
{
"matcher": "Read",
"hooks": [{"type": "command", "command": "$HOME/.claude/hooks/read-compress", "timeout": 7}]
}Adding your own permission allow-list
cp config/user.json.template config/user.json{
"permissions": {
"allow": ["Bash(gh api:*)", "Bash(pacman -Q:*)", "mcp__filesystem__list_directory"]
}
}Re-run ./install.sh to merge. User permissions are unioned with profile permissions — nothing is removed.
| Platform | Status | Notes |
|---|---|---|
| Linux | Full support | Primary development platform |
| macOS | Full support | gtimeout fallback, osascript notifications, macOS stat flags |
| WSL | Full support | Detected via /proc/version |
Cross-platform details
timeout: Falls back togtimeout(coreutils), then no-timeoutstat: Uses-c%s(Linux) with-f%z(macOS) fallbackflock: Replaced withmkdir-based locking (atomic on all POSIX)notify-send: Falls back toosascript(macOS), silently skips if neither availablerg: Falls back togrepwhere used- Binary detection: Uses
od -An -tx1 | grep ' 00'(POSIX, works on macOS/Linux/BSD)
The warden-collector is a Go service that provides session tracking, subagent budget enforcement, and OTLP span ingestion. It starts automatically on session-start and stops when idle.
Collector internals, data storage & viewer
- Session tracking — per-session token counts, model info, context window in WAL-mode SQLite
- OTLP receiver — listens on
:4319for traces from Claude Code, extractsllm_requestspans - Subagent budgets — tracks call counts and bytes; writes deny files checked by
pre-tool-useviastat() - Hook events — accepts events via
POST /v1/ingest/hookover Unix domain socket - Query API —
GET /v1/sessions,GET /v1/sessions/{id}/context(used by statusline)
Data storage — ${XDG_STATE_HOME:-~/.local/state}/claude-warden/:
| File | Purpose |
|---|---|
collector.db |
SQLite database (sessions, events, budgets, OTLP spans) |
collector.sock |
Unix domain socket for hook → collector communication |
collector.pid |
PID file for lifecycle management |
budget-deny-* |
Deny files written when subagent budgets are exceeded |
Viewer — optional htmx web UI on port 8477:
python3 viewer/warden-viewer.pyOptional Docker stack in monitoring/ for log aggregation, metrics, and tracing. Supplements the Go collector with Grafana dashboards and long-term storage.
Components, setup, data flow, dashboards
| Service | Image | Port | Purpose |
|---|---|---|---|
| Loki | grafana/loki:3.4.2 |
3100 | Log aggregation (30-day retention) |
| OTEL Collector | otel/opentelemetry-collector-contrib |
4317/4318 | OTLP logs + traces, tails events.jsonl |
| Prometheus | prom/prometheus |
9090 | Metrics |
| Node Exporter | prom/node-exporter |
9101 | Textfile collector for budget metrics |
| Tempo | grafana/tempo:2.7.2 |
3200/3205 | Trace storage |
| Grafana | grafana/grafana |
3000 | Dashboards (admin/admin) |
Start (Linux):
cd monitoring && docker compose up -dmacOS / Docker Desktop:
cd monitoring && docker compose -f docker-compose.yml -f docker-compose.macos.yml up -dDashboards — four provisioned in monitoring/grafana/dashboards/: cost/tokens/budget (claude-code-otel), tool latency/traces (warden-tool-latency), output size/tokens (warden-output-size), subagent/session lifecycle (warden-subagent-lifecycle).
Verification:
curl -s http://localhost:3100/ready # Loki
curl -s http://localhost:3200/ready # Tempo
grep tool_latency ~/.claude/.statusline/events.jsonl | tail -5
API capture
MITM proxy wrapper in capture/ for recording Claude Code API traffic.
capture/claude # interactive session
capture/claude -p "prompt" # non-interactiveLogs land in ~/claude-captures/YYYY-MM-DD/capture-HHMMSS.jsonl.
[!IMPORTANT] Bodies are truncated to 200 chars by default. Set
WARDEN_CAPTURE_BODIES=1for full capture. Sensitive keys (system,messages) are always redacted.
- Requires:
mitmdump+ trusted CA cert at~/.mitmproxy/mitmproxy-ca-cert.pem - Permissions: JSONL files created with mode 600
- Scrubbing:
x-api-key,authorization,proxy-authorizationheaders redacted
Project layout
| Path | Purpose |
|---|---|
hooks/ |
Hook scripts (bash) |
hooks/lib/common.sh |
Shared library: parsing, events, latency, cross-platform shims |
hooks/lib/otel-trace.sh |
OTLP/HTTP trace span emitter |
config/ |
Defaults, profiles (minimal/standard/strict), user overrides |
collector/ |
Go collector: SQLite, OTLP receiver, budget enforcement |
viewer/ |
htmx web UI (sessions, tokens, events) |
capture/ |
MITM proxy for API traffic capture |
statusline.sh |
Claude Code statusline script |
settings.hooks.json |
Hook config merged into ~/.claude/settings.json |
install.sh |
Install hooks, merge config, generate warden.env |
uninstall.sh |
Remove hooks, restore settings backup |
monitoring/ |
Optional Docker stack (Loki, OTEL Collector, Prometheus, Tempo, Grafana) |
tests/ |
Fixture-driven test harness |
Claude Code supports hooks — shell commands that run at specific points in the tool-use lifecycle. Hooks receive JSON on stdin describing the tool call and can:
- Exit 0: Allow the tool call (optionally with
{"suppressOutput":true}) - Exit 2: Block the tool call (stderr message is fed back to Claude as feedback)
- Output JSON: Modify tool output (
{"modifyOutput":"..."}) or suppress it
Hooks are pure bash with a single dependency (jq). They run in milliseconds. All paths use $HOME for portability. Every decision (block, truncate, compress, strip) is logged to events.jsonl with token savings estimates.
Run the test harness:
bash tests/run.shIt runs shell syntax checks, validates JSON fixtures, and executes fixture-driven behavioral assertions covering pre-tool-use blocking/allow, post-tool-use output tracking/truncation, read-compress, permission-request, and statusline rendering.
Manual checks
# Shell syntax
find hooks -maxdepth 1 -type f -print0 | xargs -0 bash -n
bash -n install.sh uninstall.sh statusline.sh
# JSON validity
jq . settings.hooks.json config/defaults.json config/profiles/*.json >/dev/null
# Exercise post-tool-use fixture (system reminder stripping)
cat demo/mock-inputs/post-tool-use-reminder-bash.json | hooks/post-tool-use | jq -r '.modifyOutput'Hooks don't seem to run
- Confirm
~/.claude/settings.jsoncontains thehookskey:jq '.hooks | keys' ~/.claude/settings.json - Start a fresh session — hooks load at startup, not mid-session.
- Commands in
permissions.allowbypass thepermission-requesthook entirely.
Read is being blocked unexpectedly
read-guard blocks bundled/generated patterns (node_modules/, dist/, minified JS) and files larger than 2MB (configurable via warden.read_guard_max_mb). Use bounded reads or find the source file.
macOS Docker Desktop networking issues
The base docker-compose.yml uses network_mode: host, which Docker Desktop does not support. Use the macOS override:
docker compose -f docker-compose.yml -f docker-compose.macos.yml up -dThis switches to bridge networking and replaces localhost with Docker service DNS names.
Hooks don't run in a specific project directory
Claude Code gates hooks on workspace trust. Until the project's CLAUDE.md is accepted at launch, all hooks are skipped. Symptom:
Skipping PreToolUse:Bash hook execution - workspace trust not accepted
Fix: create a CLAUDE.md in the project root. Press Enter to accept it on next session start.
See CONTRIBUTING.md.
See SECURITY.md for security assumptions, data handling notes, and reporting guidance.
MIT


