Configuration
claudectl loads settings from three layers (highest priority first):
- CLI flags — override everything
.claudectl.toml— per-project config in the working directory~/.config/claudectl/config.toml— global config
Show resolved config: claudectl --config
Full Example
[defaults]
interval = 2000
notify = true
grouped = true
sort = "cost"
budget = 5.00
kill_on_budget = false
[budget]
daily_limit = 25.00
weekly_limit = 100.00
[webhook]
url = "https://hooks.slack.com/..."
events = ["NeedsInput", "Finished"]
[context]
warn_threshold = 75
[brain]
enabled = true
endpoint = "http://localhost:11434/api/generate"
model = "gemma4:e4b"
# Optional model routing: a low-confidence decision from `model` is re-run on
# `escalation_model`. Unset = no routing (every decision uses `model`).
escalation_model = "gemma4:27b"
escalation_threshold = 0.7
auto = false
timeout_ms = 5000
max_context_tokens = 4000
few_shot_count = 5
[models."gpt-4o"]
input_per_m = 1.25
output_per_m = 5.0
cache_read_per_m = 0.15
cache_write_per_m = 0.9
context_max = 128000
Rule-Based Auto-Actions
Configure [rules.*] sections to automatically approve, deny, send messages, or terminate sessions based on tool name, command pattern, project, cost threshold, and error state.
Deny rules always override approve rules regardless of config order.
Event Hooks
Run shell commands automatically when session events occur:
[hooks.on_needs_input]
run = "say 'Claude needs your attention'"
[hooks.on_finished]
run = "terminal-notifier -title 'claudectl' -message '{project} finished (${cost})'"
[hooks.on_budget_warning]
run = "curl -X POST $SLACK_WEBHOOK -d '{\"text\": \"{project} hit 80% budget (${cost})\"}'"
[hooks.on_status_change]
run = "echo '[{project}] {old_status} -> {new_status}' >> ~/claude-activity.log"
Events
| Event | Trigger |
|---|---|
on_session_start |
New session discovered |
on_status_change |
Any status transition |
on_needs_input |
Session needs user approval/input |
on_finished |
Session process exited |
on_budget_warning |
Session hit 80% of budget |
on_budget_exceeded |
Session hit 100% of budget |
on_idle |
Session went idle (>10 min) |
on_context_high |
Context window usage crossed threshold (default 75%) |
on_conflict_detected |
2+ sessions share the same working directory |
on_escalation |
A third-party query was queued for your review (#446) |
Template Variables
{pid}, {project}, {status}, {cost}, {model}, {cwd}, {tokens_in}, {tokens_out}, {elapsed}, {session_id}, {old_status}, {new_status}, {context_pct}
on_escalation is the exception: it has no session behind it, so it expands no
placeholders and passes context as environment variables instead —
CLAUDECTL_ESCALATION_ID, _GRANT, _PROJECT, and _QUESTION truncated to 240
characters. See Capability Grants.
Use claudectl --hooks to verify your configured hooks.
Verified Hooks
We maintain a curated set at mercurialsolo/claudectl-hooks. To submit a hook, open an issue with the config snippet, what it solves, and any dependencies.
Claude Code Integration
Easiest path is the onboarding wizard — it covers hooks alongside budget, brain, bus, and skills in one go:
claudectl init # Interactive five-phase wizard
claudectl init --non-interactive # Same flow, no prompts (for CI / dotfiles)
The Plugin phase writes hooks into Claude Code's settings (~/.claude/settings.json by default). See claudectl init --help for --budget, --brain-url, --bus-role, and the --skip-* overrides.
For just the hook install (no other phases), the legacy flags still work:
claudectl --init # Write hooks to ~/.claude/settings.json (user scope)
claudectl --init -s project # Write to .claude/settings.local.json instead
This adds PreToolUse, PostToolUse, and Stop hooks that call claudectl --json on each event. Existing settings and hooks are preserved.
To remove:
claudectl init --remove # Soft uninstall: hooks + onboarding marker (keeps user data)
claudectl init --purge --yes # Hard uninstall: --remove + wipe ~/.claudectl/ + config file
claudectl --uninstall # Legacy hook-only removal from user settings
claudectl --uninstall -s project # Legacy hook-only removal from project-local settings
How it works
The hooks are standard Claude Code command hooks:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "claudectl --json 2>/dev/null || true", "timeout": 5 }]
}],
"PostToolUse": [{
"matcher": "*",
"hooks": [{ "type": "command", "command": "claudectl --json 2>/dev/null || true", "timeout": 5 }]
}],
"Stop": [{
"matcher": "",
"hooks": [{ "type": "command", "command": "claudectl --json 2>/dev/null || true", "timeout": 5 }]
}]
}
}
The 2>/dev/null || true suffix ensures Claude Code is never blocked if claudectl is not installed or fails.
Scope
The --scope / -s flag controls where hooks are written, matching Claude Code's own scope convention (claude mcp add -s project):
| Scope | Flag | File | Committed to git? |
|---|---|---|---|
user (default) |
--init |
~/.claude/settings.json |
No (user home) |
project |
--init -s project |
.claude/settings.local.json |
No (gitignored) |
Use user scope when you want claudectl active everywhere. Use project scope when you only want it for specific projects, or when working in a shared repo where not everyone uses claudectl.
Brain Gate Mode
The brain gate controls whether the plugin hook queries the brain on tool calls.
claudectl --mode on # Default: brain evaluates, denies dangerous ops
claudectl --mode off # Disable brain — pass through all tool calls
claudectl --mode auto # Auto-approve above confidence threshold
claudectl --mode status # Show current mode
The mode is stored in ~/.claudectl/brain/gate-mode. If the file is absent, the default is on.
The /brain command in the Claude Code plugin does the same thing:
/brain off # Disable brain for exploratory work
/brain on # Re-enable brain
/brain auto # Full auto-approve
Auto-Insights
The brain can automatically detect friction patterns and suggest workflow improvements:
claudectl --brain --insights # View current insights
claudectl --brain --insights on # Auto-generate every 10 decisions
claudectl --brain --insights off # Disable auto-generation (default)
claudectl --brain --insights status # Show current mode
The insights mode is stored in ~/.claudectl/brain/insights-mode. If the file is absent, the default is off (opt-in). When enabled, insights are generated alongside preference distillation and tracked differentially — only new patterns are surfaced.
Detected insight types: friction patterns, error loops, context blowouts, missing rules, accuracy gaps, temporal friction, cost trends.
Claude Code Plugin
claudectl ships with a Claude Code plugin in claude-plugin/ at the repository root. The plugin provides:
- PreToolUse hooks that query the brain before Bash/Write/Edit calls
- Slash commands (
/sessions,/spend,/brain-stats,/brain,/auto-insights) - A supervisor agent for proactive health triage
- A session monitoring skill that auto-activates when relevant
The plugin and the --init hooks are complementary:
| Method | What it does | Best for |
|---|---|---|
claudectl --init |
Observability hooks (PostToolUse, Stop) | Feeding data to the TUI dashboard |
| Plugin | Brain gate hook (PreToolUse) + commands | Inline approve/deny without the TUI |
You can use both. The --init hooks notify claudectl of tool completions. The plugin hook queries the brain before tool execution.
Coordination Layer
Multi-session coordination on a single machine. Stores events, leases, blockers, handoffs, interrupts, and memory in a local SQLite database at ~/.claudectl/coord.db. No TOML configuration needed — inspect with claudectl coord <subcommand>. See Reference for all subcommands.
Relay & Hive Mind Configuration
Cross-machine collaboration. Both ship in the default build: relay carries the transport and task delegation, hive (which depends on it) carries knowledge sharing. A --no-default-features --features hive build keeps the hive local. See Relay & Hive Mind for the full guide.
Both [relay] and [hive] are off until you turn them on — enabled
defaults to false in each. Every other value below is the real default.
[relay]
enabled = true # OFF by default; true starts the relay with the TUI/brain
listen_port = 9847 # TCP port for peer connections
listen_addr = "0.0.0.0" # bind address — peer transport only, not the HTTP API
max_peers = 8 # maximum connected peers
heartbeat_interval_secs = 30
reconnect_max_secs = 60
auto_connect = [] # list of "host:port" to auto-connect on startup
lan_announce = true # default; `relay serve` broadcasts on UDP 9848 so `relay discover` finds it
http_addr = "127.0.0.1" # bind address for the coordinator HTTP API
# http_port = 9876 # no default; unset means no HTTP API at all
# auth_token = "…" # no default; the API's bearer token
# lease_authority = "<peer-id>" # no default; the peer that grants fleet-wide leases
# (`coord claim --fleet`). Set the same value on every
# machine, including the authority itself.
[hive]
enabled = true # OFF by default; sharing across machines also needs relay
default_trust = 0.5 # trust level for new peers (0.0-1.0)
auto_trust_drift = true # adjust trust based on decision concordance
max_propagation = 5 # max gossip hops
export_min_evidence = 5 # min decisions before sharing a pattern
knowledge_ttl_days = 30 # expire unvalidated knowledge
inject_unverified = true # show low-trust knowledge in brain prompt
max_units = 500 # hard cap on stored knowledge units
max_prompt_units = 20 # cap on units injected into brain prompt
stale_peer_days = 90 # prune knowledge from peers gone this long
# Sharing controls — what knowledge to share with peers
share_categories = [] # empty = all shareable. Options: best_practice, technique, workflow
exclude_tools = [] # never share patterns for these tools (e.g., ["Write"])
exclude_commands = [] # never share patterns matching these substrings
http_port and auth_token have no defaults, and the coordinator HTTP API starts only when both resolve — leave either unset and there is no listener. The bind address is resolved --http-addr first, then http_addr, then 127.0.0.1. Loopback is the default because that API is plaintext HTTP/1.1 with a bearer token, so off-machine access belongs behind a tunnel rather than a wider bind (#426). listen_addr governs only the HMAC-authenticated peer transport, where 0.0.0.0 is the intended deployment. See Security.
Query Surface Configuration
Settings for the read-only project query surface. Only jev_* is configurable — the grant's own rate limit and daily budget live in the grant record, not here, so tightening one caller never loosens another. See Capability Grants.
[query]
jev_enabled = true # allow classification at all; false is the hard off
jev_model = "jev-latest" # or jev-preview, or a pinned jev-1.x.y
jev_monthly_usd = 5.00 # monthly ceiling; past it, classification degrades
# to deterministic matching rather than billing on
jev_enabled defaults to true, but it is not the on switch — a present TYPESAFE_API_KEY is. With no key the surface behaves exactly as it did before classification existed: deterministic ranking over the index, no network call, nothing leaving the machine. jev_enabled = false exists so an owner who has that key in their environment for unrelated reasons can still keep the surface local.
A configured but unavailable classifier is not the same as an absent one. If the key is set and the endpoint cannot be reached, queries answer strictly — at most 3 spans, and only spans scoring 2 or better — because an owner who enabled a gate should never get a more permissive surface when the gate is down.
The endpoint is a compile-time constant, not a config field: a redirectable classification endpoint would be a way to send a third party's question to a host the owner never approved.