# Configuration Reference

Full schema for `.claude-code-hermit/config.json`. Generated by `/claude-code-hermit:hatch`, managed by `/claude-code-hermit:hermit-settings`.

---

## Top-Level Keys

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `_hermit_versions` | object | `{}` | Plugin version tracking. Updated by `/hermit-evolve`. |
| `agent_name` | string | `null` | Display name for the agent (e.g., "Atlas"). Optional. |
| `language` | string | `"en"` | Preferred language for responses. Auto-detected from locale. |
| `timezone` | string | `"UTC"` | IANA timezone (e.g., "Europe/Lisbon"). Auto-detected. |
| `escalation` | string | `"balanced"` | Autonomy level: `conservative`, `balanced`, `autonomous`. |
| `sign_off` | string | `null` | Optional sign-off appended to channel messages. |
| `channels` | object | `{}` | Channel configuration keyed by channel name. See [`channels`](#channels) section. |
| `remote` | boolean | `true` | Enable remote control via claude.ai/code. |
| `model` | string | `null` | Override model for Claude Code (e.g., `"sonnet"`). |
| `ask_budget` | boolean | `false` | Prompt for per-session budget on session start. |
| `permission_mode` | string | `"auto"` | Claude Code permission mode: `default`, `acceptEdits`, `auto`, `plan`, `dontAsk`, `bypassPermissions`. `auto` (default) requires Max/Team/Enterprise/API plan + Sonnet 4.6 or Opus 4.6/4.7 (Max → Opus 4.7 only); not on Pro or Haiku — CC reports it unavailable at launch, switch via `/hermit-settings permissions`. See [permission modes docs](https://code.claude.com/docs/en/permission-modes). |
| `tmux_session_name` | string | `"hermit-{project}"` | tmux session name for always-on mode. |
| `auto_session` | boolean | `true` | Auto-start session on boot. |
| `boot_skill` | string \| null | `null` | Overrides the default `/claude-code-hermit:session` bootstrap that `hermit-start.py` sends into the tmux REPL on always-on launch. Set to a namespaced skill (e.g. `"/claude-code-homeassistant-hermit:ha-boot"`) to run a domain hermit's custom boot. The domain skill is responsible for calling `/claude-code-hermit:session-start` itself. Populated automatically by `/hatch` when an activated hermit declares `hermit.boot_skill` in its `.claude-plugin/hermit-meta.json`; operators can override via `/hermit-settings boot-skill`. |
| `always_on` | boolean | `false` | Set to `true` by `hermit-start`. Affects routine framing and channel behavior. |
| `quality_gate` | object | `{"tier": "budget"}` | Post-implementation quality gate at step (e.5) of `/proposal-act`. `tier` is one of: `"budget"` (never runs `/code-review`; default), `"balanced"` (delegates to the `quality-gate-judge` haiku subagent per-implementation), `"quality"` (always runs `/code-review`). Cost per implementation: Budget = $0, Balanced ≈ $0.005 (judge) + occasional ~$0.25 (`/code-review` on RUN), Quality ≈ $0.25-$0.35. Anything outside the enum or a missing key resolves to `budget` at runtime (default-safe). Tune via `/hermit-settings quality-gate`. **Note:** if you use `claude-code-dev-hermit:dev-quality` to gate commits, keep `tier: "budget"` to avoid double-firing `/code-review` (`/dev-quality` already runs it pre-commit). |

---

## `env` (Environment Variables)

Written to `.claude/settings.local.json` at boot by `hermit-start`. Exported to hooks, MCP servers, and Bash tool calls.

| Key | Default | Purpose |
|-----|---------|---------|
| `AGENT_HOOK_PROFILE` | `"standard"` | Hook profile: `minimal`, `standard`, `strict`. Protected in always-on mode. |
| `COMPACT_THRESHOLD` | `"50"` | Context % threshold for compact suggestion. |
| `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` | `"50"` | Auto-compact at this % of context window. |
| `MAX_THINKING_TOKENS` | `"10000"` | Cap on thinking tokens per turn. |
Modify with `/hermit-settings env`.

`*_STATE_DIR` vars are **not** stored in `env` — `hermit-start` derives them from `channels.<name>.state_dir` at boot.

---

## `channels`

Object keyed by channel name. Built-in channels (`"discord"`, `"telegram"`, `"imessage"`) resolve their plugin from `claude-plugins-official` automatically; third-party channel plugins set the `marketplace` field below. All channel configuration lives here — no top-level `allowed_users` or `morning_brief` keys.

| Top-level key | Type | Default | Description |
|---------------|------|---------|-------------|
| `primary` | string | _(absent)_ | Preferred channel for proactive outbound sends (e.g. `"discord"`). When set and the named channel is reachable, it takes priority. When absent, the resolver returns the first eligible entry in `channels` in operator's config order — no hardcoded slug list. Must name an existing channel key. |

Per-channel keys (under `channels.<name>`):

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | boolean | `true` | Whether this channel is active. |
| `allowed_users` | array | _(absent)_ | User ID allowlist. Absent = accept all. `[]` = accept none. |
| `dm_channel_id` | string | `null` | DM channel ID learned from the first inbound message. Used for proactive outbound notifications. Never set manually. |
| `state_dir` | string | _(absent)_ | Path to channel plugin state dir — relative to project root or absolute. `hermit-start` resolves relative paths against `cwd` and derives `<NAME>_STATE_DIR` at boot. |
| `marketplace` | string | _(absent)_ | Marketplace name for third-party channel plugins. When set, `hermit-start` builds `--channels plugin:<name>@<marketplace>` instead of using the built-in `claude-plugins-official` mapping. Required for any channel other than `discord`/`telegram`/`imessage`. |
| `morning_brief` | object | `null` | `{ "enabled": true, "time": "07:00" }` to deliver morning brief via this channel. |

Example:
```json
"channels": {
  "discord": {
    "enabled": true,
    "allowed_users": ["123456789012345678"],
    "dm_channel_id": "987654321098765432",
    "state_dir": ".claude.local/channels/discord",
    "morning_brief": { "enabled": true, "time": "07:00" }
  }
}
```

Manage with `/hermit-settings channels` (subcommands include `primary <name>` and `primary clear` to set/clear the primary outbound channel) and `/hermit-settings brief`.

---

## `heartbeat`

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | boolean | `true` | Enable heartbeat on idle transitions. |
| `every` | string | `"2h"` | Heartbeat interval (e.g., `"15m"`, `"1h"`, `"2h"`). |
| `show_ok` | boolean | `false` | Log OK results (false = silence means healthy). |
| `active_hours.start` | string | `"08:00"` | Start of active window (heartbeat pauses outside). |
| `active_hours.end` | string | `"23:00"` | End of active window. |
| `stale_threshold` | string | `"2h"` | Alert if active session has no progress for this duration. |
| `waiting_timeout` | string/null | `null` | Auto-transition from `waiting` to `idle` after this duration (e.g., `"4h"`). `null` = no timeout. |

> **Note:** The tick counter (`total_ticks`) lives in `state/alert-state.json`, not here. It is runtime state, not operator configuration.

Modify with `/hermit-settings heartbeat`.

---

## Idle & Routines

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `idle_behavior` | string | `"discover"` | What to do when idle: `"wait"` (check tasks/channels only) or `"discover"` (also run idle tasks, reflection, and priority alignment). |
| `idle_budget` | string | `"$0.50"` | Maximum cost per idle task from `IDLE-TASKS.md`. |
| `routines` | array | `[]` | Scheduled routines. Each entry: `{id, schedule, skill, run_during_waiting?, enabled}`. `schedule` is a 5-field cron expression (`minute hour dom month dow`) interpreted in `config.timezone`. |
| `monitors` | array | `[]` | Declared background watches. Each entry: `{id, description, command, class?, persistent?, enabled, timeout_ms?}`. Auto-registered at session start via `/watch start`. `class` is `"stream"` or `"poll"` (documentation label only). Runtime state in `state/monitors.runtime.json`. |

Modify with `/hermit-settings routines`, `/hermit-settings idle`.

### Cron schedule rules

The `schedule` field uses standard 5-field cron syntax: `minute hour dom month dow`.

| Field | Range | Notes |
|-------|-------|-------|
| minute | 0–59 | |
| hour | 0–23 | |
| dom | 1–31 | day of month |
| month | 1–12 | |
| dow | 0–7 | day of week (0 and 7 = Sunday) |

Atoms: `*` (any), exact value, comma-separated list (`1,15`), range (`9-17`), step (`*/15`, `0-30/10`). Named days/months and macros (`@daily`) are not supported. Both DOM and DOW non-`*` in the same expression is rejected (ambiguous semantics). Leading zeros are allowed (`09` = `9`). Step base `0` is invalid (`*/0`).

At session start (and on every `/claude-code-hermit:hermit-routines load`), each routine's `schedule` is shifted from `config.timezone` to the machine's local timezone before being registered as a `CronCreate` — which only knows about machine local time. The shift handles minute-granularity offsets, so half-hour and 45-minute IANA zones (Asia/Kolkata, Australia/Adelaide, Asia/Kathmandu) work correctly. DOW is shifted automatically when a day-wrap occurs.

DST transitions self-correct within 24h via the daily `heartbeat-restart` reload. **On the DST transition day itself, one fire may land at the wrong wall-clock hour.** If your `config.timezone` is null, schedules pass through to CronCreate unchanged (machine-local time used as-is).

Schedules that cannot be expressed as a single CronCreate after shifting are passed through unchanged with a warning: mixed day-wrap on restricted-DOW schedules, and hour step patterns (`*/7`) that lose their structure. Avoid these in routines when `config.timezone` differs from the machine TZ; split into separate fixed-time entries instead.

---

## `knowledge`

Optional. Controls domain knowledge retention and startup injection.

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `raw_retention_days` | integer | `14` | Days before raw artifacts are archived to `raw/.archive/` by the weekly review |
| `compiled_budget_chars` | integer | `1000` | Character budget for compiled knowledge injection at session start (range: 500–4000) |
| `working_set_warn` | integer | `20` | Warn in weekly review when unsuperseded compiled artifact count exceeds this |

```json
"knowledge": {
  "raw_retention_days": 14,
  "compiled_budget_chars": 1000,
  "working_set_warn": 20
}
```

Modify with `/hermit-settings`. Validated by `validate-config.js` when present.

---

## `compact`

Controls automatic compaction of SHELL.md sections during idle transitions. When a section exceeds its threshold, older entries are summarized into a single `[Earlier]` line and only the most recent entries are kept.

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `monitoring_threshold` | integer | `30` | Compact `## Monitoring` when it exceeds this many lines. |
| `monitoring_keep` | integer | `20` | Keep this many recent entries after compacting. |
| `summary_threshold` | integer | `30` | Compact `## Session Summary` when it exceeds this many lines. |
| `summary_keep` | integer | `15` | Keep this many recent entries after compacting. |

Setting `keep` equal to `threshold` effectively disables compaction for that section. Modify with `/hermit-settings compact`.

---

## `scheduled_checks`

Automatic invocations of installed plugin skills. Config stores operator intent; runtime state lives in `state/reflection-state.json` under the `scheduled_checks` key.

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `id` | string | _(required)_ | Unique identifier for this check. |
| `plugin` | string | _(required)_ | Parent plugin name (for display). |
| `skill` | string | _(required)_ | Full slash command to invoke. |
| `enabled` | boolean | `true` | Whether this check should run. |
| `trigger` | string | _(required)_ | `"interval"` (periodic during idle reflection) or `"session"` (at task completion). |
| `interval_days` | integer | `7` | Minimum days between invocations. **Required for `trigger: "interval"`. Must not be set for `trigger: "session"`.** |

### Runtime state — `state/reflection-state.json` (under `scheduled_checks` key)

Keyed by check `id`. Owned by reflect (sole writer for interval checks) and session (sole writer for session checks). Lives alongside `last_reflection` in the same file.

| Key | Type | Applies to | Description |
|-----|------|-----------|-------------|
| `last_run` | string/null | both | ISO date of last successful invocation. |
| `last_unavailable_at` | string/null | interval only | ISO date of last `unavailable` outcome (skill missing or not installed). Retries after a fixed 4-hour cooldown. |
| `last_error_at` | string/null | interval only | ISO date of last `error` outcome (skill errored or timed out). Suppresses retries for `interval_days` (persistent back-off). |
| `consecutive_empty` | integer | interval only | Consecutive runs with zero findings. Reset to 0 on any non-empty run. |

Modify with `/hermit-settings scheduled-checks`.

---

## `docker`

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `packages` | array | `[]` | System packages (`apt-get`) to install in the Docker image. |
| `recommended_plugins` | array | `[]` | Plugins to install on container boot. Empty by default — entries are only added when the operator explicitly opts in during `/docker-setup` or `/hermit-settings docker`. |

### `recommended_plugins` entry schema

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `plugin` | string | _(required)_ | Plugin name to install (substring of `claude plugin list --json` `id` left of `@`). |
| `marketplace` | string | `"anthropics/claude-plugins-official"` | Marketplace `org/repo`, passed to `claude plugin marketplace add`. The entrypoint resolves the canonical marketplace name at boot via `claude plugin marketplace list --json` to build `<plugin>@<name>` install targets. |
| `scope` | string | `"project"` | Install scope: `"project"` or `"local"`. |
| `enabled` | boolean | `false` | Whether to install on container boot. |

**Security model:** The entrypoint installs every `enabled` entry regardless of marketplace. The safety gate is at configuration time — entries are only written here when the operator explicitly confirms the list during `/docker-setup` or `/hermit-settings docker`. See [Recommended Plugins](recommended-plugins.md) for details.

Modify with `/hermit-settings docker`.

---

## Escalation Levels

| Level | Behavior |
|-------|----------|
| `conservative` | Ask before most actions. Create proposals instead of fixing. NEXT-TASK.md pickup: alert only. |
| `balanced` | Act on routine tasks, ask for significant changes. NEXT-TASK.md: auto-start. Default. |
| `autonomous` | Proceed unless blocked. Minimize interruptions. NEXT-TASK.md: fully unattended. |

---

## Environment Variable Flow

```
config.json "env"  ->  hermit-start.py  ->  .claude/settings.local.json "env"  ->  Claude Code  ->  hooks, Bash
                   ->  shell env (tmux temp file / Docker environment:)  ->  MCP servers
```

- **Shell env only:** `CLAUDE_CONFIG_DIR`, `ANTHROPIC_API_KEY` (OAuth credentials live in `.credentials.json`, written by `claude /login`)
- **settings.local.json only:** `AGENT_HOOK_PROFILE`, `COMPACT_THRESHOLD`, `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE`, `MAX_THINKING_TOKENS`
- **Derived at boot:** `DISCORD_STATE_DIR`, `TELEGRAM_STATE_DIR` — injected into both shell env (tmux forward) and `settings.local.json` by `hermit-start`, from `channels.<name>.state_dir`

---

## Complete Example

A realistic `config.json` for an always-on Docker hermit with Discord:

```json
{
  "_hermit_versions": {
    "claude-code-hermit": "1.0.6"
  },
  "agent_name": "Atlas",
  "language": "en",
  "timezone": "America/New_York",
  "escalation": "balanced",
  "sign_off": "— Atlas",
  "channels": {
    "discord": {
      "enabled": true,
      "allowed_users": ["123456789012345678"],
      "dm_channel_id": "987654321098765432",
      "state_dir": ".claude.local/channels/discord",
      "morning_brief": { "enabled": true, "time": "07:00" }
    }
  },
  "remote": true,
  "model": null,
  "permission_mode": "auto",
  "tmux_session_name": "hermit-myproject",
  "auto_session": true,
  "ask_budget": false,
  "always_on": true,
  "idle_behavior": "discover",
  "idle_budget": "$0.50",
  "routines": [
    {"id": "morning-brief", "schedule": "0 8 * * *", "skill": "claude-code-hermit:brief --morning", "run_during_waiting": true, "enabled": true},
    {"id": "evening-summary", "schedule": "0 22 * * *", "skill": "claude-code-hermit:brief --evening", "run_during_waiting": true, "enabled": true},
    {"id": "heartbeat-restart", "schedule": "0 4 * * *", "skill": "claude-code-hermit:heartbeat start", "run_during_waiting": true, "enabled": true}
  ],
  "env": {
    "AGENT_HOOK_PROFILE": "strict",
    "COMPACT_THRESHOLD": "50",
    "CLAUDE_AUTOCOMPACT_PCT_OVERRIDE": "50",
    "MAX_THINKING_TOKENS": "10000"
  },
  "scheduled_checks": [
    {"id": "claude-md-check", "plugin": "claude-md-management", "skill": "claude-md-management:claude-md-improver", "enabled": true, "trigger": "interval", "interval_days": 7}
  ],
  "docker": {
    "packages": ["python3", "python3-pip"],
    "recommended_plugins": [
      {"plugin": "claude-code-setup", "marketplace": "anthropics/claude-plugins-official", "scope": "project", "enabled": true}
    ]
  },
  "compact": {
    "monitoring_threshold": 30,
    "monitoring_keep": 20,
    "summary_threshold": 30,
    "summary_keep": 15
  },
  "heartbeat": {
    "enabled": true,
    "every": "2h",
    "show_ok": false,
    "active_hours": {
      "start": "08:00",
      "end": "23:00"
    },
    "stale_threshold": "2h",
    "waiting_timeout": "4h"
  },
  "quality_gate": {
    "tier": "budget"
  }
}
```

For a minimal interactive-only setup, most fields can stay at their defaults. Run `/hermit-settings` to see your current configuration.
