Configuration schema
The canonical JSON Schema is published at:
https://harnery.com/schemas/config.schema.jsonReference it in your config file’s $schema field to get editor autocomplete and validation in VS Code, Cursor, Zed, JetBrains, and anywhere else that respects $schema.
Locations
Section titled “Locations”Settings resolve from these sources, lowest precedence first (later wins):
~/.config/harnery/config.jsonc: the user-global file, a base for every project on the machine (honorsXDG_CONFIG_HOME; falls back to~/.config)<project>/.harnery/config.jsonc: the project settings file, which overrides the user-global file field by field (a project that sets onlyweb.portkeeps a user-globalweb.bind)- CLI flags + environment variables: per-invocation overrides (see each field’s env note)
The config reader (src/core/config.ts) merges the two files and owns binName, hooksSetupHint, hooks, agents, instructions, tools, workflow, skills, presence, coord, logs, artifacts, backup, and sync; the web layer reads files. Four values are project-only. The binName pin that harn init guards against re-stamping ignores the user file. So do agents.finalizationRoots, instructions.promptReminder, and hooks.promptContext. A global setting cannot grant filesystem authority, execute a project extension, or carry one project’s wording into another. A user-global binName is a display fallback, never a project pin.
Top-level fields
Section titled “Top-level fields”| Field | Type | Default | Description |
|---|---|---|---|
$schema |
string | none | URL to the JSON Schema (used by editors for autocomplete/validation) |
project_name |
string | repo basename | Display name surfaced in log lines and to peers |
binName |
string | harn |
Host CLI bin name used in agent-facing prompts (see below) |
agents |
object | see below | Host-owned agent ritual policy |
coord |
object | see below | Coord-layer settings |
artifacts |
object | see below | Working-artifact retention |
logs |
object | see below | Soft storage budgets for manifest-backed logs |
tools |
object | see below | Managed-tool provisioning consent (tools.ripgrep.autoInstall) |
workflow |
object | see below | Workflow-engine defaults |
skills |
object | see below | Shipped-skill suppression (skills.exclude) |
hooks |
object | see below | Optional project-owned hook extensions |
instructions |
object | see below | Host-owned agent instructions and prompt reminder |
presence |
object | see below | Cross-machine presence transports |
files |
object | see below | Web file-viewer path policy (additive deny/allow globs) |
web |
object | see below | Standalone Next.js dashboard |
backup |
object | see below | restic-backed snapshots |
sync |
object | see below | rclone-backed cross-machine sync |
binName
Section titled “binName”The host CLI’s bin name. Harnery’s coord binaries (agent-hook, agent-coord) and the web UI run as Harnery itself, so they can’t see the name of a consumer CLI that composes Harnery (e.g. myapp). They read it back from this field for every agent-facing string: end-of-turn nudges, council prompts, worktree-conflict hints. harn init stamps it automatically for a consumer (any bin name other than the harn default). Resolution precedence: HARNERY_BIN env → this field → "harn".
agents
Section titled “agents”| Field | Type | Default | Description |
|---|---|---|---|
requireGitFinalization |
bool | false |
Require the guarded Git check at the end of tool-using turns |
finalizationRoots |
object[] | [] |
Extra project-owned roots with a git or output end-turn disposition |
When enabled, automatic prompts and Stop remediation request
agents status --end-turn instead of plain agents status. The Stop verdict also
requires evidence that the guarded command ran; an ordinary status call does not
satisfy the opted-in policy. The --end-turn capability remains available for
manual use when this setting is false.
{ "agents": { "requireGitFinalization": true, "finalizationRoots": [ { "path": "../another-repository", "disposition": "git" }, { "path": "../generated-exports", "disposition": "output" } ] }}The coordination repository is an implicit git root. Every extra root comes
from the project config; the user-global config cannot grant filesystem
authority. Relative paths resolve from the coordination root. A git path must
be the repository top level. An output path explicitly approves non-Git files
and fails closed if it overlaps a Git root or the claimed path belongs to a Git
repository.
Env override per process: HARNERY_AGENTS_REQUIRE_GIT_FINALIZATION=1|0.
| Field | Type | Default | Description |
|---|---|---|---|
freshness_seconds |
number | 600 |
Heartbeat age (seconds) above which the sweeper prunes an agent (10 min) |
finalization |
object | see below | Multi-signal V3 session-finalization grace periods |
run_quality |
object | { "mode": "off" } |
Report-only run-quality observation (see below) |
Resolution: HARNERY_AGENT_COORD_FRESHNESS env → this config.jsonc key → 600.
coord.finalization
Section titled “coord.finalization”These bounds control the canonical V3 reconciler. Archive and policy signals are provisional until their grace period expires. Any new session activity cancels a pending request.
| Field | Default | Purpose |
|---|---|---|
archive_grace_seconds |
600 |
Delay before a verified archive may end a session |
idle_observe_seconds |
259200 |
Inactivity before Harnery creates an idle observation (3 days) |
idle_finalize_seconds |
604800 |
Inactivity before idle policy finalization (7 days) |
cascade_grace_seconds |
3600 |
Delay for parent, stale, superseded, and host-loss cascades |
reconcile_interval_seconds |
900 |
Default interval for agents reconcile --watch |
All values are positive integers. idle_finalize_seconds is clamped to at
least idle_observe_seconds. See ADR 0077.
coord.run_quality
Section titled “coord.run_quality”The run-quality evaluator derives advisory unknown, healthy, attention,
or critical snapshots from bounded coordination evidence. It never blocks,
steers, pauses, or stops an agent. shadow writes snapshots and transition
telemetry without exposing severity; report also adds a quality line and
JSON field to harn agents status. The package default is off.
| Field | Default | Valid range |
|---|---|---|
mode |
off |
off | shadow | report |
evaluation_interval_seconds |
30 |
5–3,600 |
snapshot_ttl_seconds |
120 |
10–86,400 |
max_tail_bytes |
2097152 |
65,536–67,108,864 |
evaluation_timeout_seconds |
30 |
1–30 |
lock_stale_seconds |
60 |
5–3,600 |
supervised_roots_per_sweep |
8 |
1–100 |
thresholds.repeated_tool_calls |
8 |
2–10,000 |
thresholds.consecutive_failures |
5 |
1–10,000 |
thresholds.context_growth_per_minute |
60000 |
1–10,000,000 |
thresholds.compaction_grace_seconds |
300 |
0–86,400 |
thresholds.no_progress_evaluations |
2 |
1–10,000 |
All supplied bounds must be finite integers. A malformed object disables the
whole evaluator for that config digest and emits one bounded
health.run_quality_config_invalid event; Harnery does not partially apply it.
The snapshot TTL must be at least the evaluation interval, and the stale-lock
bound must be greater than the evaluation timeout so a live writer cannot be
stolen inside its allowed runtime.
Snapshots live under .harnery/guard/, use a cursor separate from the session
projector, and contain hashes and aggregates rather than tool bodies. See
ADR 0075 for the signal,
wait, freshness, and rollout contract.
artifacts
Section titled “artifacts”| Field | Type | Default | Description |
|---|---|---|---|
default_retention_days |
number | 3 |
Retention used by harn artifacts create when neither --days nor --minutes is supplied |
auto_clean |
boolean | true |
Hourly-throttled cleanup at session start and before new artifact or managed QA work (same guarded deletion as artifacts clean --yes) |
max_bytes |
integer | 21474836480 |
Soft managed-store budget; valid range 64 MiB through 1 TiB |
max_unit_bytes |
integer | 1073741824 |
Per-workspace ceiling unless creation records --big; valid range 16 MiB through 1 TiB |
Resolution: HARNERY_ARTIFACT_RETENTION_DAYS env → this config.jsonc key →
3. See harn artifacts for cleanup behavior.
events
Section titled “events”| Field | Type | Default | Description |
|---|---|---|---|
rotate_active_bytes |
integer | 33554432 |
Active V3 byte threshold for starting a fresh epoch; non-positive disables rotation |
archive_max_bytes |
integer | 1073741824 |
Soft byte budget for complete closed epochs; valid range 64 MiB through 1 TiB |
archive_max_age_days |
integer | 7 |
Maximum closed-epoch age outside the newest minimum; valid range 1 through 3,650 |
archive_keep_min |
integer | 2 |
Newest complete epochs protected regardless of age or bytes; valid range 1 through 1,000 |
archive_auto_clean |
boolean | true |
Daily session-start execution of the same guarded plan as ledger-v3 archives clean --yes |
Byte and age pressure never removes the active V3 directory, a protected newest epoch, an unknown directory, a symlink, or an unreadable tree. Invalid archive settings fall back to the built-in safe defaults.
logs.storage
Section titled “logs.storage”logs.storage controls soft retention budgets for manifest-backed operational
and debug logs. It never changes roots, formats, record limits, privacy rules,
or writer behavior. It also never turns storage pressure into rejected writes.
{ "logs": { "storage": { "classes": { "operational-log": { "max_bytes": 134217728, "max_age_days": 30 }, "debug-log": { "max_bytes": 67108864, "max_age_days": 7 } }, "families": { "web-performance-log": { "max_bytes": 67108864, "max_age_days": 14 } } } }}| Field | Type | Valid range | Meaning |
|---|---|---|---|
classes.operational-log.max_bytes |
integer | 10 MiB through 1 TiB | Managed-partition byte budget for every operational log unless a family override is more specific |
classes.operational-log.max_age_days |
integer | 1 through 3,650 | Maximum sealed-segment age for every operational log unless a family override is more specific |
classes.debug-log.max_bytes |
integer | 10 MiB through 1 TiB | Managed-partition byte budget for every debug log unless a family override is more specific |
classes.debug-log.max_age_days |
integer | 1 through 3,650 | Maximum sealed-segment age for every debug log unless a family override is more specific |
families.<family-id>.max_bytes |
integer | 10 MiB through 1 TiB | Exact-family byte budget |
families.<family-id>.max_age_days |
integer | 1 through 3,650 | Exact-family sealed-segment age |
Resolution starts with the source-owned family default, applies the merged class value, then applies the merged exact-family value. The project file wins over the user-global file only at the same JSON path. A family value is always more specific than a class value, even when the family value came from the user-global file.
There is no environment-variable or mutating CLI override. Invalid values, unknown classes, and unknown project family identifiers disable log-retention planning and execution. An unknown family in the user-global file is reported as dormant because it may belong to another host project.
max_bytes counts known regular files in the managed structured-log partition.
Compatibility and legacy files are reported separately as unmanaged usage and
are never deletion candidates. The budget is soft: an active segment or
unmanaged files can leave a family over budget after all eligible sealed
segments are gone. See harn logs for the evidence fields and
harn storage for manual cleanup.
workflow
Section titled “workflow”| Field | Type | Default | Description |
|---|---|---|---|
subscriptionOnly |
bool | false (engine) / true (stamped by harn init) |
Pin workflow run to subscription billing: API-key vars are scrubbed from every child env, so children can only authenticate via the adapter CLI’s stored login |
harn init stamps { "subscriptionOnly": true } into any project without a committed workflow key; an existing workflow key of any shape is never touched (set false for key-only CI hosts). Env override per process: HARNERY_WORKFLOW_SUBSCRIPTION_ONLY=1|0. See run → billing safeguards.
| Field | Type | Default | Description |
|---|---|---|---|
ripgrep.autoInstall |
bool | false |
Let harn grep download the pinned, checksum-verified ripgrep into the harnery tools dir on first miss (falls back to GNU grep on any failure). A repo commits this once and every clone self-heals |
Env override per process: HARNERY_TOOLS_AUTOINSTALL=1|0.
skills
Section titled “skills”| Field | Type | Default | Description |
|---|---|---|---|
exclude |
string[] | [] |
Shipped Harnery skills this repo suppresses. The installed set is harn-decide, harn-council, harn-end, and harn-team; the injected AGENTS.md block uses the matching CLI command instead of an excluded skill |
| Field | Type | Default | Description |
|---|---|---|---|
promptContext.enabled |
bool | false |
Run the project prompt-context extension from the normalized prompt hook |
promptContext.timeoutMs |
int | 15000 |
Whole-provider deadline, from 1 to 120,000 milliseconds |
promptContext.maxOutputBytes |
int | 65536 |
Maximum provider stdout, from 1 byte to 4 MiB |
promptContext is project-only. Harnery ignores the user-global value because
it authorizes an executable in the repository. Enabling it expects this file:
scripts/hooks/harness/extensions/prompt-contextThe executable receives one harnery.prompt-context-request/v1 JSON document
on standard input and returns one harnery.prompt-context-result/v1 document
on standard output. Harnery validates the full response and fails open on a
missing executable, timeout, signal, nonzero exit, malformed result, oversized
output, or empty result. Provider stdout and stderr are never streamed into the
hook protocol or copied into operational logs.
{ "hooks": { "promptContext": { "enabled": true, "timeoutMs": 15000, "maxOutputBytes": 65536 } }}Claude Code, Codex, and Cursor receive validated context directly through the prompt hook.
instructions
Section titled “instructions”| Field | Type | Default | Description |
|---|---|---|---|
hostAddendumFile |
string | none | Repo-relative path to a markdown file holding this project’s own coordination policy. init splices its contents into a second managed region in AGENTS.md, init --check reports drift against it, and deinit (or deleting this key) removes the region. Harnery never parses the content |
promptReminder |
string | none | One project-owned line emitted on every supported prompt for adapters that lack native reminder behavior |
The path must be repo-relative and resolve inside the project, and the file must exist and be non-empty; anything else fails the run before the first write. Delete the key and re-run init to take the region back out.
promptReminder is project-only. Harnery ignores a value from the user-global
config so one host’s wording cannot follow the user into another project. The
line must contain 1 to 500 characters and cannot contain a newline. Harnery
reads it at emission time, passes only an enable flag through the hook process,
and does not copy the text into coordination state.
Harnery emits this line through Codex and Cursor UserPromptSubmit. Claude Code
already supplies native output-style reminders, so Harnery does not duplicate
them.
{ "instructions": { "hostAddendumFile": ".agents/host-instructions.md", "promptReminder": "Lead with the answer and preserve exact facts." }}presence
Section titled “presence”| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Cross-machine presence over git refs (refs/harnery/presence/<machine> on origin). On by default when an origin remote exists; every operation is fail-silent |
relay |
string | unset | Optional live upgrade: a wss:// relay URL (public reference: wss://relay.harnery.com; self-host via harn relay serve or relay/worker/). When set, hooks keep a per-machine daemon connected for seconds-latency presence; git refs stay on as the floor |
Env overrides per process: HARNERY_PRESENCE=1|0 (whole feature), HARNERY_PRESENCE_RELAY=<url|0> (relay only), HARNERY_PRESENCE_FETCH_INTERVAL=<secs> (git-refs fetch throttle). A committed .harnery/presence-salt file rotates the relay room (it is NOT a secret; see decision record 0016). Full behavior: harn presence.
| Field | Type | Default | Description |
|---|---|---|---|
port |
number | 4276 |
Listen port for harn web up; 4276 spells HARN on a phone keypad |
bind |
string | 127.0.0.1 |
Bind address. Localhost-only by default |
Precedence is --port, then HARNERY_WEB_PORT, then this config value, then 4276. An occupied port fails with web_port_in_use; Harnery never silently chooses a different URL.
backup
Section titled “backup”| Field | Type | Default | Description |
|---|---|---|---|
repo |
string | ~/.cache/harnery/restic-repo |
restic repository path or URL |
password_file |
string | ~/.config/harnery/restic-password |
File containing the restic repo password |
keep_daily |
number | 7 |
Daily snapshots to keep on harn backup prune |
keep_weekly |
number | 4 |
Weekly snapshots to keep |
keep_monthly |
number | 6 |
Monthly snapshots to keep |
Env overrides: HARNERY_RESTIC_REPO, HARNERY_RESTIC_PASSWORD_FILE.
| Field | Type | Default | Description |
|---|---|---|---|
remote |
string | none | rclone remote name (e.g. gdrive) |
prefix |
string | harnery |
Subpath under the remote root |
Env overrides: HARNERY_SYNC_REMOTE, HARNERY_SYNC_PREFIX.
Sync subset is fixed: identities/, journal/archived/, councils/. Heartbeats and event logs are intentionally machine-local and won’t sync.
Web file-viewer path-serving policy (the universal file viewer in harn web). The shipped secret denylist is a non-removable floor baked into code; these knobs are additive only: they extend the floor, never shrink it.
| Field | Type | Default | Description |
|---|---|---|---|
deny_globs |
string[] | [] |
Extra **/-anchored deny patterns on top of the shipped floor (treated as hard-tier: never overridable) |
allow_overrides |
string[] | [] |
Rescue globs consulted only when a deny came from a soft-tier floor pattern; naming a hard secret family is dropped at load |
How each section is resolved
Section titled “How each section is resolved”Most sections above use both config files, merged project-over-user. The
agents.finalizationRoots trust boundary comes only from project config. Env
vars and CLI flags override other config values per invocation:
| Section | Config keys | Env override | Other |
|---|---|---|---|
coord |
freshness_seconds |
HARNERY_AGENT_COORD_FRESHNESS |
(none) |
logs |
storage.classes, storage.families |
(none) | exact-family values override class values; invalid retention config fails closed |
artifacts |
default_retention_days |
HARNERY_ARTIFACT_RETENTION_DAYS |
artifacts create --days |
artifacts |
auto_clean |
HARNERY_ARTIFACT_AUTO_CLEAN |
— |
backup |
repo, password_file, keep_* |
HARNERY_RESTIC_REPO, HARNERY_RESTIC_PASSWORD_FILE |
--keep-* flags override the prune policy per run |
sync |
remote, prefix |
HARNERY_SYNC_REMOTE, HARNERY_SYNC_PREFIX |
~/.config/harnery/sync.json (written by harn sync init) is a lower-precedence fallback than the sync config section |
files |
deny_globs, allow_overrides |
(none) | read by the web layer, not the core reader |
agents |
requireGitFinalization, finalizationRoots |
HARNERY_AGENTS_REQUIRE_GIT_FINALIZATION |
finalizationRoots is project-only and has no env override |
Full precedence for a sync remote, highest first: HARNERY_SYNC_REMOTE env → project sync.remote → user-global sync.remote → ~/.config/harnery/sync.json.
Example config
Section titled “Example config”{ "$schema": "https://harnery.com/schemas/config.schema.json", "project_name": "my-monorepo", "binName": "mycli",
"coord": { "freshness_seconds": 900 },
"artifacts": { "default_retention_days": 3 },
"web": { "port": 4276 },
"tools": { "ripgrep": { "autoInstall": true } },
"workflow": { "subscriptionOnly": true },
"presence": { "relay": "wss://relay.harnery.com" },
"sync": { "remote": "gdrive", "prefix": "harnery/my-monorepo" }}Schema status
Section titled “Schema status”The schema lives at schemas/config.schema.json in the Harnery repo. v1 covers the fields above; future fields land additively with semver minor bumps. Breaking schema changes require a major bump and a migration note.