Skip to content

Configuration layers

Every himmel setting has one home and one rule for who wins. This page lists the homes, who owns each one, whether it is tracked in git, and the order in which they are read.

The layers

LayerFileOwnerTracked?Holds
Secretsthe OS keyring through scripts/lib/himmel-secrets, then legacy <primary checkout>/.envoperatornever (gitignored, unreadable to agents)API tokens, passwords, credentials, and the keys not yet classified (below)
Behaviour config~/.himmel/himmel.confoperator, reviewableyes on a luna install (a link to <vault>/.himmel/himmel.conf); a plain file otherwisenon-secret knobs: HANDOVER_DIR, CR_PROFILE, HIMMEL_INITIATIVE, …
Local override~/.himmel/himmel.conf.localoperator, this machineneverthe same keys, for one machine only
Gate policy~/.himmel/config.json, under "policy"operator only (a hook refuses agent writes)noCR_REQUIRE_CROSS_MODEL, CR_FLOOR_FALLBACK, ARMAUTOMERGE
Adopter config~/.himmel/config.json, other fieldshimmelctl install; jira.sectorField by the operatornoluna cadence, bridge, PHI declarations, the Jira Sector field id (jira.sectorField)
Install profile~/.claude/himmel/install-profile.jsonhimmelctl installnothe wizard’s answers, replayed by --from-profile
Handover registry~/.claude/handover/registry.json/handovernohandover repos, buckets, branch prefixes
Plugin and lane profilesscripts/machine-setup/ profiles, chief-kit --profilehimmel repoyeswhich plugins and tools a session loads
Chief ruleschief template, worker and judge system prompts, hookshimmel repoyeshow chiefs and workers behave
Per-chief statethe fleet manifest in the handover bucketthe chiefin the state repowhich workers are live, their docs and tokens

Status and state are never tracked as configuration: a file that changes every session (fleet manifest, queue locks, ledgers) is state, not config.

Who wins

Behaviour keys, read by load_dotenv in scripts/lib/load-dotenv.sh:

  1. the process environment (a non-empty value)
  2. ~/.himmel/himmel.conf.local
  3. ~/.himmel/himmel.conf
  4. legacy .env, with a DEPRECATED notice on stderr
  5. the consumer’s own default

Gate-policy keys, read by load_policy in scripts/lib/load-dotenv.sh:

  1. ~/.himmel/config.json .policy
  2. legacy .env, with a DEPRECATED notice
  3. unset, which every gate treats as its safe default

The process environment is not in the policy list. A gate-policy key can only change by editing a file the operator owns, so CR_FLOOR_FALLBACK=claude-only bash scripts/cr/clear-cr-marker.sh does not loosen the review gate; load_policy drops the value and prints ignoring process-env CR_FLOOR_FALLBACK. A policy key written into himmel.conf or himmel.conf.local is ignored too, because no hook guards agent writes to those files yet.

Secrets come from the resolver (next section), never from himmel.conf. A secret key planted in himmel.conf is never loaded.

A policy key exported from a shell profile (for example a sourced ~/.config/himmel/env) is process env too, so it is ignored with the same notice. Move it into config.json .policy; a behaviour key exported there still wins, as the list above says.

The directory is always $HOME/.himmel. No environment variable moves it (HIMMEL_LUNA_CONFIG_PATH, which himmelctl honours for its own fields, is not consulted for policy).

Failure behaviour

SituationBehaviour keysGate-policy keys
himmel.conf missingfall through to .env, then the defaultn/a
config.json missingn/anotice; legacy .env or the safe default
config.json not valid JSONnotice; legacy .envload_policy returns non-zero; clear-cr-marker.sh refuses (exit 14)
.env present but unreadablekey stays unsetload_policy returns non-zero; the gate refuses

himmel-doctor check C74 reports a half-migrated station: himmel.conf missing or unparseable, behaviour keys still in .env, and gate keys the policy file does not yet hold. It prints key names only, never values.

A missing file never turns a gate on its permissive side: unset policy keys leave the cross-model requirement off, the floor fallback off, and automerge off, which are today’s defaults.

Secrets: the keyring resolver

scripts/lib/himmel-secrets serves every secret-class key. load_dotenv asks it for secret keys it has not found yet, so shell consumers need no change. Order, first hit wins:

  1. the process environment (a non-empty value)
  2. $CREDENTIALS_DIRECTORY/<KEY> (a systemd unit’s LoadCredential=)
  3. the OS keyring: first the item for this project (service himmel key <KEY> project <primary checkout path>), then the user-wide item (service himmel key <KEY> scope user)
  4. legacy .env, with one DEPRECATED line on stderr naming the keys, printed only where secret-tool is installed (never on macOS)
  5. unset

The keyring is Linux only for now (libsecret’s secret-tool against a Secret Service such as gnome-keyring or KWallet); macOS falls through to .env. The resolver never prompts: a locked keyring prints himmel-secrets: keyring locked (unlock the wallet or run himmel-secrets status) and falls through within 3 s, and a cron or systemd job without DBUS_SESSION_BUS_ADDRESS finds the session bus under $XDG_RUNTIME_DIR/bus, or /run/user/<uid>/bus when XDG_RUNTIME_DIR is unset too. HIMMEL_SECRETS_KEYRING=off skips the keyring.

The secret keys of a .env that git tracks are refused with one line naming the file; only an untracked .env serves secrets.

Verbs: get KEY…, exec --keys KEY[,KEY…] -- <command> (runs the command with only those keys added to its env), status [KEY…] (the layer serving each key, names only), put KEY (value from stdin or a silent prompt; --project stores a per-project item), import-env <file> (stores each secret key of a dotenv file that scripts/himmelctl/lib/secrets-manifest.json lists, in secrets or stationSecrets, and prints names only; any other line is named by its number, and a file whose quoted value does not close on its line is refused) and export-env <file> (writes the keyring’s himmel items to a new 0600 dotenv file). block-read-secrets.sh refuses an agent’s direct get, put, import-env and export-env commands and direct keyring reads. That is not a full barrier: an agent can still reach get by sourcing the script (. scripts/lib/himmel-secrets get KEY), which HIMMEL-7585 tracks.

himmel-doctor check C75 reports the keyring state (C75-keyring) and which secret names are still served from .env (C75-secrets), names only.

Moving a station: bash scripts/lib/himmel-secrets import-env <primary checkout>/.env, then check himmel-secrets status. Leave .env in place as the fallback; import-env skips a secret the manifest does not list, so add a new one to stationSecrets first. Wait until the test runners export HIMMEL_SECRETS_KEYRING=off before you move a station whose suites run against the real keyring.

Which keys go where

Behaviour config (himmel.conf): HANDOVER_DIR, USER_SLUG, LUNA_VAULT_PATH, HIMMEL_DOC_FRESHNESS, HIMMEL_INITIATIVE, HIMMEL_INITIATIVE_OVERNIGHT, HIMMEL_OVERNIGHT, HIMMEL_WHERE_ARE_WE, HIMMEL_WHERE_ARE_WE_STALE_HOURS, HIMMEL_WORKTREE_NUDGE, HIMMEL_UPDATE_AUTOSTASH, CR_PROFILE, CR_CLAUDE_AGENTS, TICKET_ID_REQUIRED, TICKET_ID_PATTERN, TICKET_ID_EXEMPT_AUTHORS, TRACKER.

Non-secret Jira and service identifiers also belong in himmel.conf: JIRA_EMAIL, JIRA_PROJECTS, JIRA_PROJECT_KEY, JIRA_CLOUD_ID, GCP_CI_PROJECT_ID, SNYK_ORG_ID, SNYK_ORG_SLUG, VERCEL_TEAM_ID, VERCEL_USER_ID, ANTHROPIC_API_ORG_ID_A, ANTHROPIC_API_ORG_ID_B. Their meanings and defaults are unchanged; .env.example documents them. The Jira CLI reads its identifiers through these same config layers; API tokens still come only from the process env or legacy .env, never from himmel.conf or its local override.

Every reader of these keys follows the same order as load_dotenv: the process env, himmel.conf.local, himmel.conf, the legacy .env, the default. A reader that opens only .env, or only the process env, misses a key the operator has moved; the shell, the Jira CLI (scripts/jira/src/handover-root.ts), the lane resolver, the profile capture and the config UI all resolve it that way.

Gate policy (config.json .policy): CR_REQUIRE_CROSS_MODEL, CR_FLOOR_FALLBACK, ARMAUTOMERGE.

Everything else stays in .env. That includes keys whose secret-or-config class has not been decided yet: other account emails, chat IDs, cookie-file paths, service URLs and VM user names. Only the explicitly named identifiers above are config; tokens and all other unclassified keys remain secrets. A key that decides where a credential or message is sent (JIRA_BASE_URL, TELEGRAM_GROUP_CHAT_ID) stays secret while himmel.conf is unguarded.

The registry is _himmel_key_class in scripts/lib/load-dotenv.sh. A key moves from secret to config only by editing that list in a reviewed PR.

TICKET_ID_REQUIRED and TICKET_ID_EXEMPT_AUTHORS loosen the commit-message gate when set, so they are candidates for the policy list. They stay behaviour config until the commit-message hook reads them through load_policy.

Example files

Tracked templates to copy: docs/setup/himmel.conf.example (every behaviour key, commented out) and docs/setup/himmel-config.example.json (the policy section, every key null, which reads as unset). scripts/lib/test-load-config.sh checks that each template names only keys of its own class.

~/.himmel/himmel.conf uses the same KEY=value form as .env. It is parsed, never executed:

HANDOVER_DIR=/home/me/Documents/luna/handovers
CR_PROFILE=free,paid
HIMMEL_INITIATIVE=prcheck,pr,ticket,handover

~/.himmel/config.json, policy section (other fields belong to himmelctl):

{
"version": 1,
"policy": {
"CR_REQUIRE_CROSS_MODEL": "1",
"CR_FLOOR_FALLBACK": "claude-only"
}
}

himmelctl install keeps the policy field untouched and names it in one WARN line, as it does for any field outside its own schema.

Chief rules

Prose and hooks stay where they are: the chief template, the worker and judge system prompts, and the hooks are code, reviewed in the himmel repo. A rule that is a tunable number or switch moves to himmel.conf; per-chief state stays in the fleet manifest.

Example: the fleet size cap. The rule “a chief never runs more workers than the cap” stays in the chief template and its enforcing script. The number itself is a tunable knob, so it belongs in configuration, not prose. Raising it loosens a safety limit, so its home is the policy section of config.json, not himmel.conf; until it joins that list it stays a guarded process-env variable (HIMMEL_FLEET_CAP). Which workers are live right now is state, so it stays in the fleet manifest.

Feature flags

A feature flag ships new behaviour dark and lets one run turn it on. The registry is scripts/lib/flags.tsv: one row per flag (name default kind owner removal expires description). A flag is read only through scripts/lib/flags.sh (himmel_flag <name>, rc 0 on, 1 off, 2 unknown), flags.py or flags.mjs.

Resolution order, first hit wins: the process env HIMMEL_FLAG_<NAME> (1 or 0), then himmel.conf.local, then himmel.conf under HIMMEL_CONFIG_DIR, then the registry default. A flag is never read from .env.

Rules, enforced by scripts/ci/check-flags.sh on every CI event:

  • No guard behind a flag. A flag never wraps a safety rail: not under scripts/hooks/, not in a gate script, not on a ci-trust-paths.txt path.
  • 14-day expiry. expires is 14 days from birth; once it passes, CI goes red on main too. The removal sub-task named in the row deletes the flag and its branches.
  • Registered and read. A flag the code reads must be in the registry, and a registered flag must have a reader.

Current readers: chain_soft_edges in scripts/handover/chief-kit/next-dispatchable.sh.

Why this route

Two routes were compared.

Option 1: a tracked file in the luna vault, with a gitignored .local override. Settings become reviewable and travel with the vault. On its own it breaks adopters who have no luna vault: there is no tracked place for the file, and every reader would need to find the vault first.

Option 2: extend ~/.himmel/config.json, with env > config > default. Works for every install, but the file is untracked, so changes are not reviewable; and env > config contradicts the requirement that gate policy cannot be overridden from the environment.

Chosen: both, split by key class.

  • Behaviour keys use a fixed path, ~/.himmel/himmel.conf. On a luna install it is a link into the vault, so it is tracked and reviewable (option 1’s benefit). Without luna it is a plain file, so adopters lose nothing (option 2’s reach). Env still wins for these keys, as it always has.
  • Gate-policy keys use ~/.himmel/config.json, which a hook already keeps agents from writing, and their reader ignores the environment. This removes the env > config conflict for exactly the keys where it matters.
  • Nothing that works today stops working: every key still found only in .env keeps its value until you migrate it.

Policy keys can move into the tracked himmel.conf once a hook guards agent writes to it.

Migrating an existing install

The update runs the whole migration as one step for himmel’s own keys (other tools’ keys stay in .env): a dry run you confirm, a backup, the copy, a check with the real readers, and only then the removal of the moved lines from .env (updating.md). Preview it with bash scripts/setup/migrate-env.sh --dry-run. The steps below are the manual route for behaviour and policy keys.

Setup, adopt and himmelctl read and write the behaviour keys in ~/.himmel/himmel.conf (a key already set in .env is still read as a fallback), so the migration can run at any time. --dry-run lists what would move without writing.

  1. Run bash scripts/setup/migrate-env-config.sh --dry-run to list the keys it would move. It prints key names only, never values.
  2. Run it again without --dry-run (add --vault <luna vault> on a luna install to keep the file in the vault). Behaviour keys are copied into ~/.himmel/himmel.conf; secrets are not touched.
  3. For gate-policy keys it prints one command per key. Run each at your own prompt: it reads the value from .env and writes it into ~/.himmel/config.json, so the value is never shown.
  4. Start a session. When no DEPRECATED notice appears, delete the moved lines from .env. Rebuild the Jira CLI first (scripts/jira/dist is not tracked): an older build reads HANDOVER_DIR from the process env or .env only, not from himmel.conf.

The .env fallback for non-secret keys is removed one release after setup, adopt and himmelctl read the new files.

Not covered yet

A shared multi-user layer (a team’s company brain feeding settings to every member) is out of scope. It is on the backlog; the layer table above leaves room for it above himmel.conf.