Skip to content

Configuration

claudectl loads settings from three layers (highest priority first):

  1. CLI flags — override everything
  2. .claudectl.toml — per-project config in the working directory
  3. ~/.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.