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
| Layer | File | Owner | Tracked? | Holds |
|---|---|---|---|---|
| Secrets | the OS keyring through scripts/lib/himmel-secrets, then legacy <primary checkout>/.env | operator | never (gitignored, unreadable to agents) | API tokens, passwords, credentials, and the keys not yet classified (below) |
| Behaviour config | ~/.himmel/himmel.conf | operator, reviewable | yes on a luna install (a link to <vault>/.himmel/himmel.conf); a plain file otherwise | non-secret knobs: HANDOVER_DIR, CR_PROFILE, HIMMEL_INITIATIVE, … |
| Local override | ~/.himmel/himmel.conf.local | operator, this machine | never | the same keys, for one machine only |
| Gate policy | ~/.himmel/config.json, under "policy" | operator only (a hook refuses agent writes) | no | CR_REQUIRE_CROSS_MODEL, CR_FLOOR_FALLBACK, ARMAUTOMERGE |
| Adopter config | ~/.himmel/config.json, other fields | himmelctl install; jira.sectorField by the operator | no | luna cadence, bridge, PHI declarations, the Jira Sector field id (jira.sectorField) |
| Install profile | ~/.claude/himmel/install-profile.json | himmelctl install | no | the wizard’s answers, replayed by --from-profile |
| Handover registry | ~/.claude/handover/registry.json | /handover | no | handover repos, buckets, branch prefixes |
| Plugin and lane profiles | scripts/machine-setup/ profiles, chief-kit --profile | himmel repo | yes | which plugins and tools a session loads |
| Chief rules | chief template, worker and judge system prompts, hooks | himmel repo | yes | how chiefs and workers behave |
| Per-chief state | the fleet manifest in the handover bucket | the chief | in the state repo | which 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:
- the process environment (a non-empty value)
~/.himmel/himmel.conf.local~/.himmel/himmel.conf- legacy
.env, with aDEPRECATEDnotice on stderr - the consumer’s own default
Gate-policy keys, read by load_policy in scripts/lib/load-dotenv.sh:
~/.himmel/config.json.policy- legacy
.env, with aDEPRECATEDnotice - 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
| Situation | Behaviour keys | Gate-policy keys |
|---|---|---|
himmel.conf missing | fall through to .env, then the default | n/a |
config.json missing | n/a | notice; legacy .env or the safe default |
config.json not valid JSON | notice; legacy .env | load_policy returns non-zero; clear-cr-marker.sh refuses (exit 14) |
.env present but unreadable | key stays unset | load_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:
- the process environment (a non-empty value)
$CREDENTIALS_DIRECTORY/<KEY>(a systemd unit’sLoadCredential=)- 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) - legacy
.env, with oneDEPRECATEDline on stderr naming the keys, printed only wheresecret-toolis installed (never on macOS) - 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/handoversCR_PROFILE=free,paidHIMMEL_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 aci-trust-paths.txtpath. - 14-day expiry.
expiresis 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
.envkeeps 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.
- Run
bash scripts/setup/migrate-env-config.sh --dry-runto list the keys it would move. It prints key names only, never values. - 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. - For gate-policy keys it prints one command per key. Run each at your own
prompt: it reads the value from
.envand writes it into~/.himmel/config.json, so the value is never shown. - Start a session. When no
DEPRECATEDnotice appears, delete the moved lines from.env. Rebuild the Jira CLI first (scripts/jira/distis not tracked): an older build readsHANDOVER_DIRfrom the process env or.envonly, not fromhimmel.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.