Complete checklist for getting a new machine to full working state.
Adopting himmel on your own repo (not developing himmel itself)? See docs/setup/install.md instead; it covers the portable-core flow and tells you which sections here apply to you.
Support matrix
Tier
Platforms
What it promises
Supported
Linux, macOS
Linux is CI-gated on every PR (required check) — green bun-suites runs on main; adopter round trip verified on both. macOS CI runs on the nightly only (not a per-PR required check); its nightly is green — macos-cadence nightly runs (8/8 shards, 0 red suites), with the macos-cadence workflow as the ongoing check.
Not supported
Windows, WSL included
Dropped (HIMMEL-5729).
Install channels: release tarball (himmel-<version>-linux.tar.gz + .sha256, Linux): supported, steps in the README; git clone + himmelctl install: supported; plugin marketplace (marketplace/plugins/*): supported, through the Claude Code plugin marketplace (claude plugin install/update/uninstall), not himmelctl; AUR: landed (recipe in packaging/aur/); brew: not supported, reopens only if a shared macOS/Linux tap becomes worthwhile; nix / deb / rpm: not supported, reopen on user-driven demand. The tarball’s fresh-guest run is green.
One row per platform, each floor linked to the run that actually proves it.
A cell reading unverified means: no floor is declared/enforced anywhere in
the repo for that tool, or no green run exists to cite. Never assume a
value that isn’t backed by one of these links.
OS/distro
Shell
Claude Code CLI floor
git floor
node / bun
Install channels
Proof
Linux (Ubuntu/Debian/Arch/Fedora)
bash 3.2+
unverified — no minimum declared or enforced anywhere in the repo
2.31, not the declared 2.30 (git rev-parse --path-format needs 2.31+; correcting the declared floor in this doc and scripts/install/deps.json is a known follow-up)
green macos-cadence nightly runs — 8/8 shards, 0 red suites, at ba4192922 (earlier reds on 2026-09-24, 09-25 and 09-26). Floors above stay as declared: the run proves the suites pass, not a build-verified node/bun floor.
1. Required environment
scripts/setup.sh verifies these at step 0 and fails fast with install hints. Run bash scripts/setup.sh and the script tells you exactly what’s missing.
Foundational (every platform — verified at setup)
Tool
Min version
Why
bash
3.2 (most scripts) · 4.0 (8 scripts use mapfile)
macOS ships bash 3.2 — brew install bash if you hit mapfile: command not found from the 8 bash-4 scripts listed below
git
2.30+
Worktrees + --show-toplevel
node
18+ to build · 20.17+ to push
Jira plugin build + plugin-install workflow. The pre-push signature gate needs npm 11, whose own engine floor is ^20.17.0 || >=22.9.0, so a node-18 box can build and commit but cannot push.
npm
11+ (needs node ^20.17.0 || >=22.9.0)
Lockfile audit hooks + plugin install. NOT bundled on Debian/Ubuntu, and apt’s own npm is 9.2 — old enough that its expired registry key fails npm audit signatures on every push. See the Linux table.
bun
1.0+
Runs the handover armed-resume resolver, the qmd search index, the Telegram bridge, and the obsidian-triage tools. Install: curl -fsSL https://bun.sh/install | bash
python3
3.10+
realpath -m fallback (macOS) + JSON helpers in 28 scripts. PEP 668 (Ubuntu 24.04+) blocks system pip — use uv or pipx for pre-commit.
jq
1.6+
Hook input parsing (13 scripts incl. all Claude PreToolUse hooks)
sudo apt install at && sudo systemctl enable --now atd (or distro equivalent). Required by scripts/handover/arm-resume.sh for cron-armed Claude relaunches.
realpath -m (coreutils)
Default on most distros.
shellcheck, gitleaks
sudo apt-get install -y shellcheck gitleaks. Both are real apt packages on Ubuntu (verified 26.04: ShellCheck 0.11.0, gitleaks 8.16.0) — the tarball is only needed on distros that do not package gitleaks. Used by pre-commit.
shfmt
sudo apt-get install -y shfmt (or brew install shfmt, pacman -S shfmt). block-git-stash and block-graphify-egress parse a command with shfmt; with it missing they deny any command that names a stash, ref-plumbing or graphify verb, with a message naming shfmt. CI pins 3.14.1.
gh
sudo apt-get install -y gh on Ubuntu >= 24.04 / Debian >= 13 — gh is a real apt package there. himmelctl deps ensure / setup.sh’s [0/9] preflight asks whichever package manager is present (apt-get/dnf/brew) whether it has a candidate for gh, and installs it automatically when the answer is yes — not just on apt. When the selected manager has no candidate, the manual routes are the fallback: GitHub’s official apt repo (https://cli.github.com/packages) or the release tarball (https://github.com/cli/cli/releases).
uv (or pipx for pre-commit only)
curl -LsSf https://astral.sh/uv/install.sh | sh (recommended). PEP 668 blocks system pip. pipx can install pre-commit, but graphify is uv-managed — without uv, himmel-update skips graphify (status row graphify skipped uv missing) and it never installs.
npm
Not bundled. Debian/Ubuntu’s nodejs package ships without npm, so a box with a working node can still fail the install preflight (ERROR: missing required tools: npm). sudo apt-get install -y npm, or install Node + npm together from NodeSource. And apt’s npm is too old to push with — 9.2.0’s bundled registry key expired 2025-01-29, so the pre-push npm audit signatures gate refuses every push with EEXPIREDSIGNATUREKEY. Follow up with sudo npm install -g npm@11 (npm 12 requires node ^22.22.2; apt ships 22.22.1).
sudo
The toolchain steps that use apt-get need root. himmelctl deps ensure does not elevate — run the apt lines yourself (see the clean-install walkthrough below).
snyk CLI (optional)
Version on the station: 1.1308.2 (/usr/local/bin/snyk, free plan). Install from the Snyk CLI docs, then snyk auth once by hand; the token stays in snyk’s own config and himmel never reads it. bash scripts/security/snyk-scan.sh --check prints OK or a WARN and always exits 0. Used by the weekly scripts/security/snyk-scan-cadence.sh (see cadences); without it the scan reports WARN snyk and the rest of himmel is unaffected. Snyk Code additionally needs enabling in the Snyk org settings.
claudex lane (optional)
bash scripts/setup/cli-proxy-lane.sh (--install → --login → --register) brings up the codex-proxied cc-codex lane on this host — needs systemctl --user + ss (iproute2), both stock on Ubuntu/Debian/Arch/Fedora. See cli-proxy-lane.md.
macOS:
Tool
How
bash 4+
brew install bash — system bash is 3.2 and 8 scripts need 4+. Add /usr/local/bin/bash (Intel) or /opt/homebrew/bin/bash (Apple Silicon) to PATH or use #!/usr/bin/env bash (already the convention).
at daemon
Preinstalled but disabled. Enable: sudo launchctl load -F /System/Library/LaunchDaemons/com.apple.atrun.plist.
realpath -m (optional)
Macos has no realpath -m; the 5 scripts that use it (arm-resume.sh, auto-commit.sh, block-edit-on-main.sh, check-hookspath.sh) already include a python3 -c "from pathlib import Path; print(Path(p).resolve(strict=False))" fallback. Pure-GNU operators: brew install coreutils && export PATH="$(brew --prefix coreutils)/libexec/gnubin:$PATH" exposes grealpath as plain realpath.
uv (or pipx for pre-commit only)
brew install uv. pipx can install pre-commit, but graphify is uv-managed — without uv, himmel-update skips graphify.
coreutils (for gtimeout)
brew install coreutils — macOS has no GNU timeout. Without gtimeout, scripts/graphify/refresh-graph-map.sh runs graph extraction with its run deadline DISABLED (unbounded) and skips the bounded pre-pull auto-commit/fetch. Homebrew installs it as gtimeout; no PATH change is needed.
bun
brew tap oven-sh/bun && brew install oven-sh/bun/bun — bun is NOT in homebrew-core; the tap is still required (verified 2026-09 against github.com/oven-sh/homebrew-bun). scripts/machine-setup/macos.sh installs both uv and bun this way; it previously installed neither.
Fleet CLIs — auxiliary post-install items
Installing the CLI fleet on a new station (claude, codex, hermes,
copilot, grok) leaves a set of one-time auxiliary steps that the plain
installers do NOT cover. Calibrated on two stations, 2026-07-12:
Item
Step
Notes
hermes home
export HERMES_HOME=$HOME/.hermes for scripts/hermes/invoke.sh calls
Newer hermes installs use ~/.hermes; the chokepoint’s resolver does not probe it yet (a known limitation).
hermes profile
bash scripts/hermes/install-himmel-profile.sh (with HERMES_HOME set), then hermes profile use himmel_agent
Installs the senior-reviewer profile + parity_guard hook.
hermes hooks
Approve the parity_guard hook once in a live hermes session
Hook approval is interactive; one-shots inherit it afterward.
hermes channels
Keep chat channels (telegram etc.) enabled: false on stations
ONE poller per bot token, fleet-wide — the host’s always-on bridge owns the channel. A station gateway that polls the same token 409-conflicts it.
provider auth
Merge/verify the providers your lanes need in ~/.hermes/auth.json (e.g. the openai-codex provider for gpt-6.1-sol one-shots)
A fresh login may create fewer providers than your primary machine has.
codex
codex login (ChatGPT OAuth), then codex login status
copilot
First run is interactive: login + approve the repo as a trusted folder
Config files alone (config.json/settings.json) are not sufficient.
grok
grok login (browser OAuth) per machine
A non-authed station hangs at “Waiting for authorization…” on one-shots.
jq
sudo apt install -y jq (Debian/Ubuntu; use your distro’s package manager elsewhere)
Required by most himmel hooks; not preinstalled on minimal distros.
pre-commit hooks (non-himmel clones)
pre-commit install --hook-type pre-commit --hook-type commit-msg --hook-type pre-push in any OTHER repo clone on the station
A bare pre-commit install leaves commit-msg + pre-push gates silently absent. The himmel checkout itself no longer needs this: himmelctl install/scripts/setup.sh place all three hook types by default.
Linux clean install, end to end (verified on Ubuntu 26.04, 2026-09-02)
Walked end to end on an Ubuntu 26.04 guest, 2026-09-02. Every failure, message
and workaround quoted below is reproduced verbatim from that run; where himmel’s
own tooling could not do a step, it is called out rather than smoothed over, and
the workaround is the documented path until the linked ticket lands.
Two honest caveats about the walkthrough itself. The guest was not pristine: it
carried a ~/.claude and an older himmel checkout from earlier test runs, which
is why the hook-rewiring behaviour below could be observed at all. And steps 1-3
are the standard documented installs for tools that box already had (uv,
pre-commit, bun, the claude CLI): they are listed so the sequence is complete,
but the commands exercised on this run were steps 0, 0b and 4 onward. Everything
after step 4 was executed exactly as written.
Terminal window
# 0. Prerequisites apt owns. node alone is NOT enough: Ubuntu's nodejs
# package has no npm, and the install preflight hard-fails without it.
nodescripts/himmelctl/bin.jsinstall--dry-run|teedryrun.txt# the plan
nodescripts/himmelctl/bin.jsinstall# the real thing
(--dry-run is teed to a file because the headless workaround below needs the
profile JSON it prints; on a box with a terminal you can drop the tee.)
The install trusts the workspace it just configured:
without this, the permissions.allow/permissions.additionalDirectories
entries it just wired into .claude/settings.json are silently ignored until
a human runs claude interactively here once. If the trust write itself
fails (an unwritable ~/.claude.json, say), the install summary’s still manual section names it. Run claude interactively in the clone once and
accept the trust dialog, or set projects["<abs clone path>"].hasTrustDialogAccepted: true in ~/.claude.json yourself; the summary’s remedy prints the exact key
to use.
Two rough edges you will hit on this path today. Both are filed; both have
a workaround that completes the install.
Run the install in user scope when the adopted project is the himmel
clone itself.
The default derivation picks project scope with the target set to the
current directory, and the portable-core copy then tries to copy the
checkout’s files onto themselves:
cp: '.../scripts/hooks/auto-approve-safe-bash.sh' and '.../scripts/hooks/auto-approve-safe-bash.sh' are the same file
Answer user at the scope question (or pass a profile with
"scope": "user"). User scope skips the portable-core copy entirely and the
install then completes: plugins, marketplaces, the Jira CLI build, the
statusline, the launcher, and the hook wiring in ~/.claude/settings.json.
A headless / SSH-only box needs --from-profile, and that path does not
save the profile.
With no TTY the real install refuses (non-interactive install requires --from-profile <path>), while --dry-run prints a complete profile it does
not save. Capture the printed JSON block, install from it, then seed the
cache by hand so himmelctl status and ensure work afterwards:
Terminal window
sed-n'/^{$/,/^}$/p'dryrun.txt>profile.json# the block --dry-run printed
On a box with a terminal, answer the questions interactively instead and
neither step is needed: the interactive path writes the cache itself.
himmelctl deps ensure does not finish the job on Linux. On a stock
Ubuntu box it reports 0 installed, 4 failed: its apt-get calls run
unprivileged, gitleaks has no automated recipe, and bun/qmd are not re-resolved
on PATH within the same pass. Install those four from step 0 and step 2 above
and himmelctl status goes green on them.
Expect reds you did not cause. A lean adopter’s first himmelctl status
lists rows for opt-in capabilities that were never selected; hermes-checkout
in particular reports a Windows AppData path on Linux. Reds for
graphify, rtk, and telegram-bridge simply mean those
optional pieces are not set up; none of them blocks the core loop. A clean
starter install’s optional Jira (jira-env-keys) and Bitbucket
(bitbucket-cli-build, unless the repo’s origin is on Bitbucket Cloud or
FORGE=bitbucket, where an unbuilt CLI stays red) rows, and a not-yet-created
inline handovers/ dir
(handover-wiring), read n/a, not red; the
himmelctl status count line says how many n/a rows were wanted but not set
up.
The installer places these hooks by default; pass
--skip-hooks to opt out. What it places depends on whether the target repo
already has a .pre-commit-config.yaml:
Your own config present: the installer runs pre-commit install and leaves
your config untouched; the hooks run whatever you configured.
No config: the pre-commit framework’s hooks would skip on every commit and
push (“config file not found”), so the installer writes himmel’s native
commit-msg, pre-push and pre-commit gates directly instead. Those gate
without any config file.
himmelctl status reports pre-commit-hooks as red when the installed hooks are
framework stubs with no config to run. Ticket discipline itself is default-on.
Two things to know once they are placed:
Ticket discipline is on by default. The commit-msg gate enforces
conventional-commit shape always, and a ticket reference unless you opt
out. The pattern comes from TICKET_ID_PATTERN if set, else
JIRA_PROJECT_KEY (PROJECT-123), else himmel’s own #N enumeration
(#123). That enumeration is the ticket system for a repo without Jira,
not a relaxed fallback, and /handover new-epic / /handover new-task
allocate the next free number for you. To turn the ticket half off entirely,
set TICKET_ID_REQUIRED=0 in the repo’s .env or the environment; the
conventional-shape check stays on either way.
The lockfile gate finds bun for itself now. It used
to depend on who ran git commit: a hook inherits the environment of the
invoking git process rather than building a fresh PATH, so a commit from your
own terminal worked (that shell sourced ~/.bashrc, where bun’s installer put
~/.bun/bin) while the same commit from an SSH command, a script, CI or an
agent died with ERROR: bun did not report a version on any commit,
docs-only included. The gate now looks in PATH, $BUN_INSTALL/bin and
$HOME/.bun/bin in turn, and when it still finds no bun it refuses only the
commits that stage something under the bun package; a docs commit is skipped
with a line naming everywhere it looked. Nothing to do on a normal install.
The symlink below is still worth making: it fixes the same blind spot for
every other non-interactive consumer (cadences, scheduled runs, himmelctl deps ensure), since ~/.local/bin is exported by ~/.profile, which has no
interactive-only early return, though that only helps a launching chain
that sources ~/.profile in the first place (a login shell), not a bare
ssh host 'cmd' or a cron job with neither PATH layer; see
the two PATH layers:
Terminal window
ln-sf~/.bun/bin/bun~/.local/bin/bun
Your first push will take a few rounds, and each refusal is real. On the
guest it took three attempts, in this order:
npm audit signatures refused because apt’s npm is 9.2 (step 0b above);
fixed by sudo npm install -g npm@11. The gate checks npm --version before
the signature check and says so in as many words; on an npm
predating that fix you get the raw EEXPIREDSIGNATUREKEY per package
instead, which is the same problem wearing a supply-chain costume.
It refused again for scripts/jira only: “found no dependencies to audit
that were installed from a supported registry”, because the worktree
bootstrap had populated node_modules without registry provenance. npm ci
in scripts/jira fixes it; that is exactly the remediation the gate names.
The CR gate refused once to install its own ref-stream hook
(pre-push.legacy) and told you to retry; the retry passes.
Then it goes through. The attestation gates (“Cross-platform attestation”,
“Security-review attestation”) pass first-try as long as the trailers are in
the FIRST commit. Do not try to add them with git commit --amend after a
refusal.
worktree.sh new leaves the new worktree dirty. It installs the Jira CLI
dependencies, which rewrites scripts/jira/package-lock.json. Revert that file
rather than committing it: git checkout -- scripts/jira/package-lock.json.
PATH: ~/.bun/bin will look missing even when bun works. See
the two PATH layers.
This is why a probe can report bun (or claude) missing on a machine where
both are installed and working.
Linux station (CachyOS, first fleet day 2026-09-04)
Distinct from the Ubuntu walkthrough above: these surfaced running himmel as
the daily driver on an Arch-family main station, not from a fresh install.
Lean plugin floor + local override. See Lean profile
above for how settings.local.json overrides the floor in both directions.
The lesson on this box: toggling a plugin off with claude plugin disable
alone does not survive the next reconcile if its key is still true in
settings.local.json: flip the override, not just the live toggle.
/plugins → “enabled in project settings but isn’t installed.” A
migrated ~/.claude/settings.json can carry false keys for plugins never
installed on this machine: reconcile-enabled-plugins.sh’s whitelist
logic carries any unknown live key forward as false rather than deleting
it, so a dead entry survives every reconcile untouched. That’s an
enabledPlugins cleanup, not an install problem:
f=~/.claude/settings.json; t=$(mktemp "$f.XXXXXX") && jq 'del(.enabledPlugins["<name>"])' "$f" > "$t" && mv "$t" "$f"
(a mktemp beside the target, never a predictable cwd-relative name).
Toggling a community plugin.claude plugin enable/disable <name> is the
sanctioned toggle. himmelctl config set hooks.plugin.<name> only knows the
fixed table in scripts/machine-setup/full-plugin-enable.json (the
lean-profile re-enable list above); it rejects any other plugin name, so
reach for claude plugin directly for anything outside that list. That
toggle alone doesn’t survive the next reconcile, though (see
Lean profile
above): claude plugin enable <name> also needs "<name>": true under
enabledPlugins in ~/.claude/settings.local.json, or the next reconcile
drops it back to the floor; claude plugin disable <name> mirrors this with
false there, or the next reconcile brings it back.
arm-resume on Linux. Needs at + a running atd. On Arch/CachyOS:
sudo pacman -S at && sudo systemctl enable --now atd; cronie is the
weaker fallback when at is absent (see
§4c above). The POSIX
launcher in scripts/handover/arm-resume.sh’s schedule_arm submits an
at -t <timestamp> heredoc job whose body is a plain claude ... --model ... invocation: no controlling terminal, nothing opens a window.
--dry-run is side-effect-free (it prints the exact job body and “dry-run
complete (no changes made)” without touching the at queue) and is the safe
way to see what an arm will actually run. Because the fired session has no
TTY/stdin, any permission-gated action it hits resolves as a silent DENY at
rc=0 after a ~3s stdin watchdog rather than hanging. Pass --automerge if
the arm must push a dependency chain through green PRs unattended (it sets
ARMAUTOMERGE=1 + CR_MERGE_GATE_OK=1 on the launched session); without it
the script only WARNs and the dependency chain stops at the first green PR. .env’s
ARMAUTOMERGE is only the default used when --automerge was not passed
on this invocation; an explicit flag always wins over it.
Jira CLI. No env-assignment prefix on the invocation other than a bare
literal JIRA_PROJECT_KEY=<KEY> and the innocuous
LANG/LANGUAGE/LC_*/TZ locale prefixes:
block-jira-compound-write.sh refuses any other prefix shape, not Jira writes
themselves. The sanctioned invocation is the plain node <repo-root>/scripts/jira/dist/index.js <op>, which picks the key up from the
primary checkout’s .env (see
Required .env values
above); pass --project only for cross-project work.
Station traps, one line each. Arch ships /usr/bin/pre-commit and
co-locates node with coreutils in /usr/bin; a hermetic-PATH test fixture
that pins PATH=/usr/bin:/bin or scrubs the PATH must stub these tools
out, not assume scrubbing /usr/bin removes only what the suite intends (it
also silently drops xargs). Scripts tracked from Windows commits land mode
100644 (no +x) and fail direct invocation with rc=126 on Linux; a
check-shebang-scripts-are-executable pre-commit hook now guards new commits
from reintroducing this. run-shell-tests.sh’s machine lock leaking
SUITE_LOCK_WAIT into test-suite-concurrency.sh (a queued after-report
inheriting an unrelated wait budget instead of the suite’s own) is fixed.
Verify hooks actually fire on a dash-/bin/sh box (Debian/Ubuntu; Arch’s
/bin/sh is bash, so this is silent here too): from inside a himmel clone,
dash -c 'command -p sh scripts/lib/run-node.sh -v' prints a version,
not nothing. Then run dash scripts/lib/run-node.sh -v as well. Two
probes because a launcher resolves to /bin/sh: the first pins the shape
production actually wires, but on a bash-/bin/sh box it executes
run-node.sh’s own body under bash, so a dash-incompatible expansion inside
the file sails straight through it. The second puts that body under dash. A
bash-only expansion anywhere in the chain aborts silently at exit 0 instead. A third probe pins the PATH boundary:
env PATH=/nonexistent /bin/sh -c 'command -p sh scripts/lib/run-node.sh -v'
(the outer shell by ABSOLUTE path; env clobbers PATH before it could look
up a bare dash)
must ALSO print a version; a bare sh there dies rc=127 before any guardrail
runs, which is why hooks are wired command -p sh.
(command -p sh, not .: POSIX leaves . with operands beyond the filename
unspecified and dash DROPS them: dash -c '. probe.sh a b' sees zero
arguments where bash sees two. A dot-wired run-node.sh therefore ran a
bare node with an empty argv, and every hook was a silent no-op on
Debian/Ubuntu; hook commands are now wired
command -p sh "…/run-node.sh" <args> and
run-node.sh refuses loudly with rc=2 on an empty "$@" rather than
exec’ing a payload-less node. Invoked this way $0 names
run-node.sh, so it resolves resolve-node.sh from its own directory and
no CLAUDE_PROJECT_DIR is needed. The older sourced probe
CLAUDE_PROJECT_DIR="$PWD" dash -c '. scripts/lib/run-node.sh' sh -v
still works: operands placed after the -c string become the shell’s OWN
positional params, which a sourced file inherits, and without one of $0,
the plugin root or CLAUDE_PROJECT_DIR set run-node.sh silently takes its
own fail-open path instead of printing anything.)
Scripts requiring bash 4+
These will error mapfile: command not found on macOS system bash (3.2). Either install bash 4+ via brew install bash, or convert the mapfile use to a while IFS= read loop (cheap port if needed):
scripts/luna/sweep-himmel.sh
scripts/hooks/check-mcp-plugin-refs.sh
scripts/hooks/check-lockfile-integrity.sh
scripts/hooks/check-npm-audit.sh
scripts/hooks/check-npm-audit-signatures.sh
scripts/hooks/check-npm-licenses.sh
scripts/hooks/check-uv-lock.sh
Every other script is bash 3.2-compatible.
Single-source tool dependencies
These tools show up in only one script, easy to drop if not on PATH (the affected feature degrades):
cygpath (Windows-only path translation) → scripts/handover/arm-resume.sh and scripts/lib/qmd-bin.sh (junction creation); falls back to the unconverted path when absent.
shellcheck, gitleaks → scripts/machine-setup/ubuntu.sh bootstrap only; pre-commit framework re-fetches them per-hook so they don’t need to be on PATH for normal use.
pipx → fallback in setup.sh + ubuntu.sh only (uv is primary).
qmd → setup.sh step 4 only (collection register); optional if you don’t use qmd search.
scripts/setup.sh copies .env.example → .env for you (§4); you
then fill in your own values. Run setup with --fill-env to be prompted for each one; the prompt prints a short help
blurb (what the value is + where to get it, sourced from the .env.example
comments) before each field, so you don’t have to read the file first. Press
Enter at a prompt to keep the current value; non-interactive shells skip the
prompt entirely.
Two kinds of variable live in .env and they reach code differently; this
is the classic “I set it in .env but nothing happened” tax. The same split is
documented at the top of .env.example:
TOOL-LOADED: a himmel CLI reads .env itself, so a value sitting in the
file is enough; no shell export needed.
PROCESS-ENV: hooks, shell scripts, and skill tools read the live
environment, not the file. Export these in the shell that launches claude,
or set them in ~/.claude/settings.json"env": {}. A value sitting only in
.env is not seen, except the ones bridged from .env by
scripts/lib/load-dotenv.sh (HANDOVER_DIR, USER_SLUG, CR_PROFILE, the
HIMMEL_INITIATIVE* set, HIMMEL_WHERE_ARE_WE + _STALE_HOURS (but NOT
the statusline-only _ROLLUP_TTL / _SEG_TIMEOUT knobs, which are live-env
only), HIMMEL_DOC_FRESHNESS and the nudge flags; the full bridged list
lives in the .env.example header), noted
below. A third class, SESSION-ONLY (guardrail bypasses like
EDIT_ON_MAIN_OK, per-launch opt-ins like TELEGRAM_OWN_POLLER), is never
read from .env at all: set in the launching shell; inventoried in
.env.example’s SESSION-ONLY section.
TOOL-LOADED (a value in .env is enough):
Variable
What it is
Where to get it
USER_SLUG
Operator slug — names handover bucket paths + registry.json. Falls back to your git user.name slugified if unset. Also bridged into the live env.
Atlassian admin console, or the getAccessibleAtlassianResources API.
ZAI_API_KEY
Z.ai GLM key for the claude-glm launcher of the GLM lane, which is dormant (HIMMEL-1967; the lane registry offers it only when GLM_LANE_OK is set) (see tooling-catalog). The launcher takes the live process env first, else reads it from .env. It cannot distinguish shell env from settings.json-injected env — don’t put this key in settings.json (that hands it to every session); use per-launch shell env or .env.
z.ai account → API keys. Optional — only if you use claude-glm.
Jira Sector (optional)
A sector is a stable, coarse product area: the “where in the system” axis of a
ticket, next to epics (“what goal”) and versions (“when”). Tagging every ticket
with one lets a board or report answer “where is our effort going, and which
area is starved?” without hand-tagging; together with the epic-or-standalone
rule it stops orphan tickets. Sectors are a custom Jira field, so they are off
until you configure them.
node <repo-root>/scripts/jira/dist/index.js sector setup --dry-run prints
the explanation, a suggested nine-sector set and the exact Jira calls.
... sector setup (add --sectors "A,B,C" for your own set, --yes to skip
the prompt) creates the single-select field and its options and prints
a {"jira": {"sectorField": "customfield_NNNNN"}} snippet; merge it into
~/.himmel/config.json yourself (the setup command never writes that file;
the field id is not a secret, so it is not a .env key).
... sector setup --opt-out prints the "sectorField": "none" snippet
instead. With it absent or none, create neither requires nor sends a Sector; the
epic-or-standalone rule stays, and edit --sector and edit-batch refuse.
The valid options are read live from the field, so adding one in Jira needs no
code change.
PROCESS-ENV (export, or set in settings.json"env"; a value only in
.env is not read unless it’s a bridged exception):
Variable
What it is
Where to get it / note
HIMMEL_REPO
Path to your himmel checkout.
Auto-set by setup/adopt into settings.json; set explicitly only for a non-default clone path.
HANDOVER_DIR
Handover state root (Mode B — external state repo).
Run /handover-setup. Bridged from .env. Unset = inline <repo>/handovers/.
LUNA_VAULT_PATH
Luna vault root for end-session capture.
Your Obsidian vault path. Not bridged — export it. Unset → ~/Documents/luna.
CLAUDE_LANE_AUTO_RESEED
Optional opt-out for the lane launchers’ config auto-refresh. By default claude-codex (and the dormant claude-glm) re-seeds its isolated config dir when your ~/.claude settings or plugin registry changed, so lane workers pick up plugin-profile changes automatically. Set 0 to restore the once-only seed (first launch + explicit --reseed) if the auto-refresh ever blocks a launch in your setup.
Leave unset (default on). Not bridged from .env — set it in the launching shell. Only relevant if you use the offload lanes.
Not a himmel .env variable: the ollama binary itself reads it, so it must
be set at OS/user scope (not just exported in the shell that launches
claude) to reach a background ollama service. The primary zero-egress
guarantee for the ollama-local lane (see /lanes) is structural and holds
without this: bare model names never reach cloud, cloud is opt-in only via
the -cloud suffix. OLLAMA_NO_CLOUD=1 is an additional belt-and-suspenders
pin, checked (advisory, never a hard fail) by /himmel-doctor.
OS
Set command
macOS
launchctl setenv OLLAMA_NO_CLOUD 1 (current login session) — also add export OLLAMA_NO_CLOUD=1 to your shell profile so it survives reboots
Linux
Add export OLLAMA_NO_CLOUD=1 to your shell profile, or if ollama runs as a systemd user service, a drop-in: systemctl --user edit ollama.service → Environment=OLLAMA_NO_CLOUD=1
For the full inventory (optional Bitbucket, Confluence, VM, and hermes keys,
the automated review / pr-check critic profile (CR_PROFILE), handover/AFK (unattended) mode tuning,
Telegram bridge flags, and the SESSION-ONLY guardrail-bypass list), read the
annotated .env.example; it is the single source of
truth (the complete operator flag map) and every entry there
carries its own inline guidance.
2. Global Claude Config
These files live at ~/.claude/ and must be copied on every new machine.
CLAUDE.md uses @RTK.md; both files must be present in ~/.claude/.
The model key carries no [1m] suffix
~/.claude/settings.json’s model key sets the station-wide default model.
Write it bare: "model": "claude-fable-5-1", never "claude-fable-5-1[1m]".
Say why precisely, because the measurements below do not support the stronger
claim: a suffix there is a station-wide default nobody chose per worker, and it is
not even a reliable one. Fable, the very family in the example, silently
strips [1m], so that exact string set nothing at all; and dropping the suffix
did not move plain launches off a 1M window. So write it bare as
configuration hygiene (the key should make no station-wide claim about context
mode) and set the mode where it is actually observable, at arming.
Context mode is an arming-time choice instead: --context 1m|standard on
scripts/handover/arm-resume.sh (7th positional on headed-arm.sh), defaulting
to standard for every arm, chief or not; a chief (armed
through chief.sh, which has no --context flag of its own) can still
opt into 1m via CHIEF_CONTEXT=1m in the launching shell. The measured
behaviour of both levers (which model ids accept the suffix, what a plain launch
actually reports, and why --autocompact is the half that does the work) is in
docs/internals/lane-calibration.md.
Two variants of the working principles, activated by profile
The general engineering defaults (think before coding / simplicity first /
surgical changes / goal-driven execution) are not in himmel’s project
CLAUDE.md; that file is held to a byte budget and carries himmel invariants
only (gate scripts/ci/check-claude-md-budget.sh). They ship at
user scope instead, in one of two ways:
Variant
Who gets it
How
Operator
this machine and any other operator station
the manual cp above — global-claude-md.md is the full personal file, including @RTK.md
Adopter
bash scripts/adopt.sh --profile core --scope user, user scope only
wire_user_claude_md appends the fenced block from user-scope-claude-md-template.md to both~/.claude/CLAUDE.md (Claude Code) and ~/.codex/AGENTS.md (Codex), creating each file if absent
The adopter path is idempotent and never destructive: it skips when the
HIMMEL:working-principles marker is already present, appends (never
overwrites) when it is not, and also skips on a heuristic phrase match so a
hand-written target that already states the principles is left alone. It runs
against both targets, but only under --scope user: a --scope project install must not touch anything under $HOME, so a
project-scope adopter who also wants the principles runs adopt.sh --scope user separately. A Claude-only install would otherwise leave a Codex adopter
with the principles nowhere. Hermes is not a
target because it already carries the same four principles in its
himmel_agent profile SOUL (scripts/hermes/assets/himmel-agent.SOUL.md,
§ How you work), installed by install-himmel-profile.sh. Re-running
adopt is also the migration path for an install that predates this, and adds
nothing otherwise.
Existing installs also get a best-effort backfill on the harness self-update
path: scripts/himmel-update.sh’s advisory steps run the same two-target
wire_user_claude_md install unconditionally on every himmel-update (not
under --check), so an adopter who already ran adopt.sh before the block shipped
picks the block up on their next update without re-running adopt by hand. Like
the other advisory steps, it never fails the update.
3. RTK (Rust Token Killer)
RTK is a token-saving CLI proxy that wraps common commands.
Terminal window
# Install (Linux / macOS)
cargoinstallrtk# or download binary from releases
# Verify (must show rtk, NOT reachingforthejack/rtk)
After himmel machine-setup, rtk init --show reports:
[--] Hook: not found
[warn] settings.json: exists but RTK hook not configured
And every rewritten command prints [rtk] /!\ No hook installed to stderr.
Both are benign and expected. himmel replaces rtk’s bare rtk hook claude
PreToolUse entry with scripts/hooks/rtk-hook-guard.sh, which
proxies rtk internally and adds a compound-predicate filter to fix broken
find rewrites. rtk’s self-check looks for its own rtk hook claude signature
(which the guard replaces), so it can’t find the hook even though rewriting is
fully operational. rtk gain will show real token savings accumulating.
Do NOT run rtk init -g to “fix” it. That re-adds the bare entry the setup
already replaced. The next run of reconcile-rtk-hook.sh collapses it back,
but there’s no need to create the problem in the first place.
If bare entries do accumulate (e.g. you ran rtk init -g outside of
machine-setup), run the idempotent reconciler:
himmelctl is the install wizard and the primary install path.
It runs the preflight + required-tools gate, then walks you through a few
questions and derives + runs the right underlying command. A missing required
tool (git/jq/python3, or a JS package manager) is auto-fetched via the platform
package manager when possible, else the wizard fails loud and points you at
the required-environment table.
Question
Choices
Notes
profile
starter | luna | operator | custom
Replaces the old adopter/contributor role fork. Every install now walks the SAME question set; profile only SEEDS the default answer at each later question below (a numbered menu with per-option help text) — it never skips one. Default starter. The contributor-dev overlay is set only by the --contribute flag, never a question (see below).
scope
project | user
Universal now (no longer role-gated).
vault
none | default-template | existing
none scaffolds no vault; default-template scaffolds a luna vault from template — if the destination already exists it is refused unless it carries the luna stamp (adopt.sh would skip the copy and silently adopt whatever is there), and a stamped destination is reused with the summary saying so rather than claiming a scaffold; existing wires a STAMPED luna vault (non-luna→luna conversion deferred). Skipping adopt.sh/setup.sh wiring entirely (no pre-commit hooks/guardrails/statusline/env.HIMMEL_REPO — operator wires manually) has no in-wizard option: don’t run the wizard.
handover
inline | external
external persists HANDOVER_DIR to an external state repo.
pluginSet
lean
The only option (the default). full was dropped: its enable set didn’t reflect what himmel actually runs — see §6 for the documented per-plugin manual recipe instead.
lanes
codex | hermes | none (comma list)
Universal now. Default none. v1 ships Claude tiers as the only implementation lanes — codex and hermes are offered ONLY as cross-model automated review lanes for /pr-check, never as implementation lanes.ollama-local and copilot-cli — the two lanes this question used to default-select — are dropped from the wizard entirely; they still exist as ordinary (now dormant) rows in scripts/lanes/lanes.json, reachable only by an operator who opts in directly with that registry’s optInEnv (OLLAMA_LOCAL_LANE_OK=1 / COPILOT_CLI_LANE_OK=1), never through himmelctl. The interactive menu carries a help line per option naming what it needs (e.g. “codex — requires the codex CLI + its own login; skip if you don’t have it”) — selecting either AT THE PROMPT is itself the explicit consent --with-codex / --with-hermes provide non-interactively. The --lanes CSV flag stays restricted to none only: naming codex/hermes there is still refused (so the flag never becomes a second, quieter door around that consent), and naming ollama/copilot there is refused the SAME way — one door, not two — with the refusal naming the lanes.json opt-in env instead of pretending a --with-ollama flag exists. Without codex or hermes selected (by either door), /pr-check’s review panel runs Claude-only and CR_REQUIRE_CROSS_MODEL cannot be satisfied — the wizard discloses this at the question and the summary reflects the resulting floor.
alwaysOn
yes | no
Universal now. Default no. Chooses the machine-hardening checklist over a one-line pointer — nothing is executed either way (see below).
cadences
per-cadence multi-select: pipeline, qmd, graphmap, drift-fix, upstream-watch, doctor, pull-cadence — comma list or none
PER-ROW gated, not whole-question vault gated: pipeline/qmd/graphmap are offered only when vault≠none; drift-fix/upstream-watch/doctor carry requires:'none' and are ALWAYS offered (they need neither a vault nor a lane); pull-cadence is offered only once GOOGLE_HEALTH_REFRESH_TOKEN is set in the primary checkout’s .env (docs/luna/google-health-connector-setup.md) — asked only when at least one row is offered, so e.g. a vault-less machine still gets asked (offering drift-fix/upstream-watch). Replaces the old binary luna-cadence question: pick WHICH recurring cadence jobs to arm now, not all-or-nothing. Enter accepts the profile-seeded recommended set (operator seeds pipeline,qmd,graphmap; every other profile seeds none). pipeline arms via the flags himmelctl derives from luna.cadence.* (see Adopter config file below); every other row arms via its own script’s all-default invocation (no per-adopter schedule surface yet). luna.cadence.enabled mirrors the pipeline selection for older readers. If the config document could not be saved it refuses to arm any unit, rather than leaving machine state the config cannot account for. scripts/graphify/graph-cadence.sh is NOT a separate row — it has no cmd_arm of its own; arming graphmap already arms it unconditionally as a fifth, standalone task.
disarm cadence
yes | no
Asked only when at least one offered cadence above was declined, naming every declined unit. Default no. A yes runs each declined unit’s own disarm subcommand with consent (--dry-run shows it) to tear down any already-armed jobs; a no leaves them armed and the run’s summary says so explicitly, per declined unit, rather than implying off already disarmed anything.
PHI declaration
yes | no
vault≠none only. Default no. Preceded by a printed, read-only PHI checklist (§4d). Records only the operator’s yes/no answer at luna.phi.declared — himmelctl creates none of the PHI markers themselves (see §4d).
secrets walk
run | skip
vault≠none only. Default skip. Walks luna secrets interactively (instruction card + a probe per secret); himmelctl never harvests the secret value itself. Walks only the secrets whose feature tag (scripts/himmelctl/lib/secrets-manifest.json) matches what you actually selected elsewhere in this run — vault≠none for luna-source credentials, telegram bridge on for TELEGRAM_BOT_TOKEN/WHISPER_MODEL. A feature you declined (or never asked about) is skipped, with one line naming how many secrets and why; .env.example’s own generated block stays the full union for every adopter, reorganized into the same per-feature sections.
telegram bridge
off | on
vault≠none only. Default off. Configures the bridge for voice/text ingestion (see §8.6).
bridge .env path / whisper CLI / whisper model
paths (blank = default/autodetect)
Asked only when the telegram-bridge answer above was on. Defaults: ~/.claude/channels/telegram/.env, autodetect, ggml-small.bin.
bridge persistence
yes | no
Asked only when the bridge is onand this platform has an installer: a systemd --user unit (telegram-bridge.service) + linger (Linux). Default no. On any other platform (macOS included) the question is skipped entirely and himmelctl prints a one-line notice to install/enable persistence by hand instead.
Flags: --dry-run prints the derived plan without executing; --from-profile <path> replays a saved answer profile non-interactively (the wizard caches your
answers, so the same install replays verbatim); --scope project|user (below)
runs non-interactively too, from a shipped profile rather than a saved one;
--contribute layers the
contributor-dev setup.sh primitive on top of the install (never a
question); --lanes <csv|none>,
--with-codex and --with-hermes answer the lane question up front (all three
are refused alongside --from-profile; a saved profile already carries its
lane selection and stays the sole authority on a replay). To offboard later:
node scripts/himmelctl/bin.js uninstall, a thin wrapper over
scripts/uninstall.sh (§8.7).
Non-interactive install.--default-scope only presets a
wizard answer; the wizard still prompts. To skip the wizard entirely, run
one of the shipped, placeholder-free adopter profiles:
Terminal window
nodescripts/himmelctl/bin.jsinstall--scopeproject# or: --scope user
This is shorthand for --from-profile docs/setup/profiles/adopter-<scope>.install-profile.json: a plain starter,
pluginSet: lean install with no lanes, no vault, and no telegram bridge (see
docs/setup/profiles/README.md). Point --from-profile at your own saved or
hand-authored profile instead for anything more; combining --from-profile
with --scope overrides just that profile’s scope field.
What the adopter profile does NOT do (v1, deliberate). Two
things it reports rather than performs, because in both cases doing them would
mean the installer asserting something it cannot stand behind:
Lanes are probed, never installed or force-activated. The wizard resolves
each selected lane through the same evaluator and the same merged
configuration /lanes uses (scripts/lanes/probe.mjs + resolve.mjs, base
lanes.json overlaid with lanes.local.json), so the two can never
disagree; notably hermes is detected via $HERMES_PY / venvs under
$HERMES_HOME, not by looking for hermes on PATH, and a lane you have
turned off locally reports DISABLED (with the config set command that
re-enables it) rather than available, and a corrupt lanes.local.json
makes lanes report UNKNOWN naming that file rather than quietly falling
back to the base registry. A single overlay entry that is degenerate (a
null/unknown-kind probe) fails closed the same way the resolver does: the
lane reads not-usable with the reason named, because /lanes excludes it
too. Whenever a lane needs an overlay fix, the summary lists that fix
and the lane’s own setup step, in that order, including the case where
a lane is both missing and locally disabled, where installing the CLI
alone would leave /lanes still excluding it. Under the resolver’s
LANES_REGISTRY test override the same replacement-no-overlay semantics
apply here, so the report never describes a different lane set than
/lanes resolves. Physical presence is always decided by the base
registry probe, never by the overlay: an override can turn a lane off, but
config set lanes.<id> on cannot conjure a binary you never installed;
that combination reports MISCONFIGURED and keeps its install command.
Note the probes establish that a binary
exists, not that a lane is configured: hermes has no richer readiness
probe, so a present hermes binary always stays listed under still manual
as a verify item (confirm the venv/auth manually). Only a lane with a richer
readiness probe (codex, via the manifest’s provisioning check) can report
ready. A lane whose
CLI is already present is reported as binary already present and skipped (so a re-run is
idempotent); a lane that is absent is listed under still manual with its
install command. That command and the follow-up setup step point at
scripts/codex/install-himmel-codex.sh and the POSIX branch of
docs/hermes-runbook.md. It is NOT
installed for you. An applied adopter install writes the top-level
profileAllowlist and profileAllowlistScope fields to lanes.local.json;
selected lanes keep their real base probes. It does not call
himmelctl config set lanes.<id> on, because that would write
probe.kind=always and make an absent lane report as available.
Machine hardening is printed, never executed. With alwaysOn=yes the run
ends in a checklist of the
Phase-6 steps: power profile, no-lock-on-wake,
sshd, auto-logon. They are machine-wide, need an elevated shell, and
himmelctl uninstall cannot undo them, so they stay a checklist you run by
hand.
Every run ends with an honest summary: what was installed, what was skipped and
why, and what remains manual. Under --dry-run the first bucket is labelled
would install and the installed list is empty; a preview never reports
work it did not do.
On an applied adopter install, the selection narrows the runtime /lanes
inventory. Non-selected optional lanes whose real probes succeed remain visible
under suppressed-by-profile; they are physically available but not routable.
No profileAllowlist means the pre-profile resolver behaviour is unchanged.
Node-less (clean) machine?himmelctl install itself needs node. Run the
bootstrap shim first. It installs node (+ bun on macOS via brew; apt has no
bun package, so Linux gets node + npm and bun stays an optional post-bootstrap
step) via the platform package manager (brew/apt) then hands off to
himmelctl install (himmelctl’s own preflight covers every other tool):
Terminal window
bashscripts/himmelctl/bootstrap.sh--dry-run# Linux/macOS: preview the plan
bashscripts/himmelctl/bootstrap.sh# then run for real
Deprecated setup shim: scripts/machine-setup/ubuntu.sh. This full-machine provisioning script is soft-deprecated
as an install entry point. They still provision the complete toolchain (git /
python / jq / shellcheck / gitleaks, nvm+node, uv, the Claude CLI, rtk, with zero
capability loss, locked O4) but no longer do himmel/luna wiring themselves;
they print a deprecation notice and delegate the wiring to
himmelctl bootstrap → himmelctl install. Prefer himmelctl install (above)
on a machine that already has node, or the bootstrap shim on one that doesn’t.
The hard-remove of this now-deprecated shim script itself (once its
toolchain-provisioning role is also absorbed) is deferred to a follow-up.
What the wizard runs under the hood no longer depends on a role (that
fork was removed): scripts/adopt.sh (the portable-core flow; see
docs/setup/install.md) always runs.
Passing --contribute layers scripts/setup.sh on top of it afterward
(pre-commit install, Jira CLI build, .env from .env.example, plugin
install, and wiring the statusline + env.HIMMEL_REPO + the UNIVERSAL
hooks into ~/.claude/settings.json, user scope; see §4b), never instead
of adopt.sh, and never asked as a question.
Build artifacts are gitignored: build them after cloning.scripts/jira/dist/index.js and scripts/bitbucket/dist/index.js are TypeScript
build outputs, NOT tracked. A fresh clone has no dist/, so a direct
node scripts/jira/dist/index.js list dies with MODULE_NOT_FOUND. bash scripts/setup.sh
builds them (step [3/10], Jira + Bitbucket CLIs). If you skip setup, build by hand:
Terminal window
cdscripts/jira && npminstall && npmrunbuild && cd../..# or: bun install && bun run build
nodescripts/jira/dist/index.jslist# verify
# same shape for the Bitbucket CLI: cd scripts/bitbucket && npm install && npm run build
To update an existing checkout later, run /himmel-update (or bash scripts/himmel-update.sh):
git pull is what delivers himmel updates; marketplace autoUpdate does not.
See updating.md.
Diffs desired state (the manifest at scripts/install/manifest.json, crossed
with your recorded install state) against actual state (live probes),
severity-grouped (red/degraded/green/n/a). Read-only: never prompts;
never mutates on repeat runs (the first check for a given target may
persist one derived state entry: the sanctioned derive-write). --items
scopes the run to a comma-list of item ids; --json emits stable
machine-readable output instead of text.
Six luna/telegram-bridge items probe subsystems most
adopters never opt into, so a fresh install must not read as a wall of red
for something never turned on:
id
what it checks
cadence-armed
whether the configured cadence schedules are actually registered with the OS scheduler
luna-sources
each configured fetch-health source, via fetch-health.py --probe <source>
phi-coherence
a .salus marker under the vault vs. its listing in phi-roots — reports degraded on a mismatch, never red
engine-allowlist
armed cadence jobs’ required engines against cadence-approve-engines.sh --print-engine-list
bridge-health
access.json + a live getMe call + exactly one getUpdates consumer (it counts poller.ts command lines anchored to THIS checkout, so a supervisor plus its poller child is one consumer, not two). Linux/macOS prefer systemctl --user show telegram-bridge.service and walk its MainPID’s children (pgrep -f <poller path> when no unit exists). Any other platform reports degraded naming process identity as unverified, never a silent pass
bridge-persistence
whether bridge persistence will actually survive a restart
bridge-persistence is pass/warn/opt-in; it never reports a hard red once
opted in. A query that FAILS reports degraded/unknown, never absent: “not
there” and “could not ask” stay distinct results. On Linux it checks
unit-installed + enabled + linger, each with its own degraded reason (a
unit that installs but whose linger step fails is reported as a partial
install with the remediation named, never auto-rolled-back; without
loginctl enable-linger, the systemd user unit only runs while that user is
logged in and stops at logout). On macOS it always reports degraded, naming
launchd as Stage 2; an unverified persistence state is never reported as
healthy. When bridge.enabled is not true in ~/.himmel/config.json,
bridge-persistence and bridge-health report a clean absence (n/a), not a
failure: opting out is not a fault.
Three items scoped by your recorded SELECTIONS, not a config
flag.telegram-bridge, hermes-lanes and codex-cli carry no
~/.himmel/config.json flag of their own (unlike the six above), so their
only record of whether you ever opted in is the cached wizard answers read
from install-profile.json (see below). A missing credential/install for a
feature you never selected (telegram bridge off, the hermes/codex lane
not chosen) reports n/a, not red; a genuinely selected-but-absent one still
reports red. A machine with no recorded profile at all keeps today’s full-nag
behavior rather than going quiet (fail-open, never silent). The same
selection→feature mapping (adopterProfileLib.resolveActiveFeatures()) also
scopes the wizard’s own secrets walk (previous section).
The scope comes from your last wizard run (the cached
~/.claude/himmel/install-profile.json): under project scope the target
is keyed by the directory you run it from, so run it from the adopted
project’s root; under user scope the target is the single user entry
regardless of cwd. (A per-run scope override lands with the Phase-2 verbs.)
It’s the shared pre-check/post-check/enable-time primitive later himmelctl
verbs (install/uninstall/enable) build on.
Three steps: a status-shaped pre-check read, driving whatever it reports
red/degraded through the EXISTING install primitives (adopt.sh/setup.sh/
the wire-*.sh libs/install-plugins.sh/qmd’s qmd_install, never
reimplemented), then a status-shaped post-check. Idempotent: an
already-green target is a no-op. --profile reconciles the target to a
different profile FIRST (so e.g. ensure --profile luna on a core-derived
target converges the luna-only items too) before converging. --items scopes
the run to a comma-list of item ids. Both status --items and ensure --items
reject an unknown item id outright (exit 2); what ensure --items adds
(unlike status, which validates ids and nothing further, staying a pure
read/display filter) is that it validates the
selection’s dependency closure in BOTH directions before touching anything: a
selection that omits a still-desired, red/degraded prerequisite an item being
converged depends on is rejected (exit 2, zero mutation), naming both ids
and the remediation that actually resolves it; the mirror check on the
toward-disabled side rejects unwiring a prerequisite while a still-desired
dependent isn’t being unwired too. Naming a prerequisite in --items is not
by itself sufficient to satisfy this check if that prerequisite is hint-only
(see below, and the “never fail-closes” caveat there); only a prerequisite
that will genuinely converge this run does. --dry-run prints the
ordered convergence plan and makes zero mutations: no primitive is ever
invoked. Converging and disabling are separately consented, and disabling is
opt-in. A default ensure converges only: it never disables
anything, whatever the target’s state says. Removing wiring requires
--prune, and even then it is a second, independentProceed with disabling? [Y/n], asked after the converge confirm and declinable on its own;
declining it still lets the convergence proceed. Each disable candidate is
printed with per-item evidence naming what the recorded profile says, what the
live probe found, and the exact command the disable would run; not enabled for this target (profile/scope) alone is not something an operator can
consent to. When the recorded profile looks under-recorded relative to live
state, ensure says so and refuses to offer mass disables even under
--prune, pointing at the wizard rather than reconciling reality downward.
Declining the converge confirm aborts the run outright: the --prune pass is
not reached, and the message says so. That asymmetry is deliberate: “no” to
the first prompt reads as “stop”, and following it by offering to remove
things would be a worse answer, not a more independent one.
This replaces the pre-2349 behaviour, in which one consolidated
himmelctl: about to ... line could list convergence work (“converge N
item(s): …”) and disable/unwire work (“disable N item(s): …”)
semicolon-joined, and a singleProceed? authorized everything listed
including the unwiring. That bundling is the incident this fixes: an
operator had to accept eight unwanted disables to take one wanted
convergence. A non-interactive, non-dry-run run
(piped/automation, how ensure runs outside a Claude session) that has
any work to perform (convergence, disable, or both) and passes neither
--yes nor --dry-run exits 2 with non-interactive ensure requires --yes and makes zero mutations; automation must pass --yes explicitly;
there is no silent unattended consent. An already-green run (nothing to
converge or disable) needs no consent and exits 0 whether or not --yes
is given.
An item with no automated install path yet (a config-type item, pending
sub-ticket D, or one with no install descriptor at all) is reported as a
hint. Two distinct guarantees here, not one: once a run is actually
converging, a hint-only item never fail-closes it on its own account: it’s
never dispatched, so ensure never waits on or blocks over its success.
But --items dependency-closure VALIDATION (above) is a separate, EARLIER
gate: naming a dependent whose desired+red prerequisite is hint-only is
rejected before anything runs at all, precisely because that prerequisite
can never converge no matter how it’s selected. So a hint-only item CAN
block a selection from proceeding in the first place, even though it never
blocks convergence once a run is genuinely underway. An enabled item the
operator no longer wants converges by its removable field: per-item runs
the matching unwire-*.sh primitive; full-offboard-only errors, naming
the item and pointing at himmelctl uninstall.
HIMMELCTL_SUDO_PASSWORD — optional, for unattended Linux dep installs
ensure can converge missing host binaries (dep-type manifest items:
jq, git, shellcheck, …) via the platform package manager. On Linux
that means apt-get, which needs sudo. HIMMELCTL_SUDO_PASSWORD is an
optional var: set it to let an unattended ensure run authenticate and
install via apt non-interactively; leave it unset and ensure falls back to
the non-interactive sudo -n form, which never prompts and never hangs:
a dep whose sudo would PROMPT for a password fails FAST rather than blocking
forever waiting for interactive input nobody outside a Claude session is
there to provide. sudo -n is not an unconditional failure: where sudo
is already cached from a recent authentication, or the account is configured
NOPASSWD, it succeeds and the dep installs normally; the var only matters
when authentication would actually prompt. Resolution order mirrors every other himmel
.env-backed setting: a live HIMMELCTL_SUDO_PASSWORD in the process
environment wins; otherwise it’s read from the primary checkout’s.env. Without it configured, ensure prints a one-line advisory (once per
run, never per dep item) pointing here; it never reveals whether the var
was merely absent or present-but-empty, only “not configured”.
Security: the password is passed to sudo on stdin only (sudo -S -p ''), never as a command-line argument (ps-visible to every user on
the box), never logged, never printed in a --dry-run preview or an error
message, and never present in the child process’s environment either:
ensure passes every apt/brew primitive an explicit, scrubbed copy
of its own environment with HIMMELCTL_SUDO_PASSWORD deleted. An
inherited env var would otherwise be readable from /proc/<pid>/environ by
any other process running as your user for the primitive’s whole lifetime,
and would propagate to every grandchild it spawns (apt hooks, postinst
scripts, …), none of which need it. A timed-out primitive is torn down
tree-wide (not just the direct child) so a wedged installer does not leave a
credential-bearing descendant running in the background, but the two
platforms give materially different guarantees, and the POSIX one is
weaker: on Linux/macOS the installer runs in its own process group, killed
as a group on timeout, which is best-effort: a descendant that
deliberately leaves that group (a setsid, a double-fork: routine daemon
behaviour) is no longer reached by the group kill and survives it. Do not
read the POSIX path as a Job-Object equivalent; it has no comparable
structural teardown.
Because the value lives in a plaintext .env file, treat it like any other
secret in that file:
The primary checkout’s .envmust stay untracked; it is already
listed in himmel’s .gitignore; never git add -f it or copy its
contents into a tracked file.
Give it owner-only permissions on Linux/macOS: chmod 600 .env.
Where available, prefer a real secret manager (your OS keychain, pass,
a vault/secrets-manager integration, CI secret store, …) over a
plaintext .env value. Set HIMMELCTL_SUDO_PASSWORD in the process
environment from there instead of writing it to disk at all; recall the
resolution order above (a live process-env value always wins over the
.env fallback), so this requires no code changes, just not writing the
file-based fallback.
macOS needs no equivalent for a standard Homebrew install: the default,
supported prefix (/opt/homebrew on Apple Silicon, /usr/local on Intel)
is owned by the invoking user, so brew install never prompts for a
password there. A custom prefix, or an install directory with
pre-existing permission problems, can still require elevation or fail
outright. HIMMELCTL_SUDO_PASSWORD has no effect on brew either way (it’s
only ever consumed by the Linux sudo -S apt-get path), so a brew failure
of that kind needs manual remediation.
The three generic guardrails (auto-approve-safe-bash, block-edit-on-main,
block-read-secrets) can live at the user scope (protects every repo you
work in) or the project scope only. himmel ships them project-level, so if
you also wire them at the user scope they would fire twice inside himmel: a
doubled bash spawn per tool call. setup-hooks.sh --guardrail-mode manages the
himmel-owned user-level block so exactly one layer is active:
Terminal window
# global (default): protect all your repos; inside himmel the user-level copies
# run through guardrail-skip-in-himmel.js (one cheap node spawn, no double bash).
bashscripts/setup-hooks.sh--guardrail-modeglobal# add --yes to skip the prompt
# project: drop the user-level block (himmel-only protection; single native spawn).
bashscripts/setup-hooks.sh--guardrail-modeproject
A bare bash scripts/setup-hooks.sh installs the git hooks and only prints the
current mode. /himmel-update reports if a global block’s baked node path drifts.
After setup, fill in your .env values; see the per-variable walkthrough in
§1 Required .env values, or
re-run setup with --fill-env to be prompted with inline help for each field:
Terminal window
# Fill in Jira token (or run setup with --fill-env for guided prompts)
vi.env# set JIRA_API_TOKEN=...
# Verify
nodescripts/jira/dist/index.jslist
pre-commitrun--all-files
Adopter config file — ~/.himmel/config.json
The luna-cadence, PHI-declaration and telegram-bridge wizard answers above are
written to a single shared adopter config document, ~/.himmel/config.json
(override: HIMMEL_LUNA_CONFIG_PATH), the same path on every platform. This
is not the wizard’s own private answer cache
(~/.claude/himmel/install-profile.json, mentioned above); that file
replays your wizard answers on a --from-profile run; this one is the
runtime config document the luna/bridge subsystems themselves read. Writes
are atomic (write-to-temp, validate, rename) and keep one timestamped .bak.
Schema v1: these are the fields himmelctl manages; only a schedule’s day
is optional. A key not in this table (top level or nested) is yours: an
install keeps it untouched and prints one WARN naming it; it never fails
the install. A value of the wrong type for a field below is
still refused:
luna.phi.declared records only the adopter’s yes/no answer to the PHI
question above; himmelctl creates none of the PHI markers themselves; §4d
below stays the authority on those (the .salus marker, the phi-roots
listing, the egress-denylist entries).
The wizard writes only the fields the run actually supplied. An
unsupplied section (a contributor run, or an adopter who answered
vault=none and so never saw these questions) is left exactly as loaded on
disk, never coerced to a default; the write is skipped entirely when nothing
changed.
Older or hand-trimmed files are migrated, not rejected. If
the file lacks any field in the table, the install fills each missing field
from the default above (or from this run’s own wizard answer, where it asked),
after backing the file up byte-exact to config.json.bak-<timestamp> and
recording that backup in the install provenance ledger. It prints one
himmelctl: migrated ~/.himmel/config.json: <field> = <value> (default | wizard answer)
line per field. A value you set is never dropped or rewritten, known key or
not. A second run finds nothing missing, so it writes nothing and makes no
second backup; --dry-run reports the migration and writes nothing. A file
that is not valid JSON is refused and left untouched; the error names the
hand-fix command (cp <file> <file>.hand-fix.bak && $EDITOR <file>).
Precedence (governs WHISPER_CLI / WHISPER_MODEL / TELEGRAM_ENV): a
process environment variable overrides the configured value, which overrides
the hardcoded default above.
4a. Optional — single-writer opt-out
For personal repos that commit straight to main (e.g. luna, salus, your docs/state repo),
opt them out of the block-edit-on-main hook by dropping a local .single-writer
marker at each repo’s root. The marker is gitignored (via global excludes) so it
never propagates to a clone; a checkout without it stays protected.
Terminal window
# Single-writer opt-out for block-edit-on-main: ignore the marker
# globally (once), then drop one in each single-writer repo.
This lets those repos opt out of the on-main edit block locally without the marker
ever being committed.
4b. Hook scope: user vs project
himmel’s hooks split into two scopes. scripts/setup.sh (and adopt --scope user) wires the UNIVERSAL set into your user-scope~/.claude/settings.json
so they apply to every Claude session, in any directory, not just inside the
himmel clone:
UNIVERSAL (user scope):auto-approve-safe-bash, block-edit-on-main,
block-read-secrets (PreToolUse) + inject-initiative (SessionStart). Without
user-scope wiring, a session launched outside the repo has no auto-approve (so
the allow-listed Jira CLI gets denied) and no auto-ship-step injector (so HIMMEL_INITIATIVE
never fires).
HIMMEL-DEV-ONLY (project scope):check-cr-marker-on-pr-create,
block-backend-tier, auto-arm-on-cap, check-update-available, …; they only
make sense while working inside the himmel repo, so they stay in the repo’s
committed .claude/settings.json and are not user-wired.
The hooks reference this clone’s absolute path and dedup by hook basename, so
re-running setup after moving the clone repairs the wiring instead of double-wiring.
A fresh contributor who clones himmel still gets the safety hooks from the committed
project .claude/settings.json even before running setup.
Duplication is benign. Inside the himmel repo the UNIVERSAL hooks are wired at
both scopes and fire twice; that is idempotent (two auto-approve passes = the same
allow; two block passes = the same block), so setup stays silent about it in-repo.
For another adopted project that also carries a project-scope copy, setup prints
an advisory listing the dupes + the unwire-pretooluse-hooks --scope project --target <repo> command to collapse them (never automatic).
HIMMEL_INITIATIVE and the AFK (unattended) pair are read from the himmel clone’s .env
by the SessionStart hook (a value exported in the launching shell or set in
settings.json env still wins); they ship commented in .env.example, so the
opt-in default-OFF is preserved; uncomment one line to enable.
scripts/uninstall.sh step [6/8] is the symmetric teardown: it removes exactly
what setup/adopt wired (preserving your non-himmel keys); --skip-settings keeps it.
4c. Scheduler backend (auto-arm resume)
The usage-cap auto-resume (auto-arm-on-cap → arm-resume.sh) schedules a
relaunch via the OS scheduler. The watchdog hook is wired above, but the
scheduler backend it relies on must exist or the armed resume silently never
fires:
Linux: needs at + a running atd. ubuntu.sh installs+enables it
(prompted); or do it by hand: sudo apt install -y at && sudo systemctl enable --now atd. (crontab is only a weaker fallback when at is absent.)
macOS: uses crontab (arm-resume skips at/atrun, which is
off-by-default / SIP-fragile). Run scripts/machine-setup/macos.sh (ALPHA:
validate it fires and file an issue). cron may need Full Disk Access on
modern macOS.
Diagnose any existing install with /himmel-doctor (the C9-scheduler check
reports OK / WARN + the exact per-OS remediation; it never runs a privileged
command).
4d. Optional — PHI vault marker for claude-glm
The GLM lane is dormant (HIMMEL-1967; the lane registry offers it only when
GLM_LANE_OK is set). If you still use the claude-glm launcher (Claude Code on the Z.ai GLM
flat-rate lane; see tooling-catalog),
drop a .salus marker file at the root of every PHI-bearing vault (e.g.
~/Documents/salus). The launcher refuses to start (exit 3, no override)
when the marker sits in the directory you launch from; the check is
per-directory, not subtree: launching from a subdirectory of a marked
vault does not see the marker. For a hand-rolled PHI vault the marker is
not placed by any himmel script; create it by hand (a vault scaffolded
via setup.sh --medical gets it automatically):
Terminal window
touch~/Documents/salus/.salus
For whole-subtree coverage (any launch cwd under the root refused), also
list the absolute PHI roots one-per-line in ~/.config/claude-glm/phi-roots:
same PHI-tier refusal, but subtree-wide.
4e. qmd search bootstrap (optional)
Skip if you don’t use qmd semantic search over the himmel docs + luna vault. Optional; the harness runs without it.
qmd is a local markdown search engine (BM25 + vector + rerank). himmel’s clone
runs it as a shared HTTP daemon (localhost:8181) auto-brought-up
by the qmd plugin’s SessionStart hook, so every session shares one read-only
index. The standalone CLI installs from a local clone of the carried fork
yotamleo/qmd until tobi/qmd#1029 is fixed upstream (the
launcher forwards signals to bun, upstream PR tobi/qmd#1030; a follow-up pins
back to tobi/qmd, and re-running the install re-points an existing clone’s
origin in place), pinned to an immutable commit SHA
rather than a mutable branch. Never bun add -g @tobilu/qmd
directly, which EPERM-wedges on this project’s machines (zombie qmd mcp
stdio processes hold locks) and bun blocks its postinstall script. bun itself is
still required to build the clone (project rule: bun, never npm; see §1
foundational table). bash scripts/setup.sh step [4/10] + adopt.sh’s
wire_qmd_core already run this install, register the himmel collection,
and best-effort qmd pull; this section is the manual bootstrap if you
skipped those or want the luna vault indexed too.
Terminal window
# 1. Install the qmd CLI: clone the fork, build it with bun, then symlink it
# onto the bun-global @tobilu/qmd path —
# scripts/lib/qmd-bin.sh is the single chokepoint (also used by adopt.sh/
# setup.sh); repo/ref/clone-dir are overridable via QMD_FORK_REPO /
# QMD_FORK_REF / QMD_FORK_DIR. Idempotent — re-run to update.
The qmd Claude plugin ships a path stub in ~/.claude/plugins/cache/qmd/qmd/<v>/bin/qmd
that references an unbuilt dist/; scripts/lib/fix-qmd-stub.sh (run by setup +
adopt) rewrites it to locate the bun-global install so plain qmd works everywhere.
Stop the shared daemon: qmd mcp stop. The index is sqlite+WAL read per query, so
docs added by qmd update are live immediately; no daemon restart needed.
4f. cc-codex / cc-glm offload shims
scripts/shell/himmel-offload-shims.sh provides the cc-codex and cc-glm
Bash functions (cc-glm launches the dormant GLM lane, HIMMEL-1967). bash scripts/setup.sh installs the
guarded source line in ~/.bashrc best-effort; install it directly with:
Terminal window
bashscripts/shell/himmel-offload-shims.shinstall
The resulting ~/.bashrc line sources the checkout’s absolute
scripts/shell/himmel-offload-shims.sh path only when that file exists.
5. Luna Vault
Skip if you don’t use the Luna vault (the author’s personal Obsidian vault). §§5a, 6, 7, 8, 8.6 all depend on it; skip those too.
Canonical layout (after the layout fix): the vault lives at ~/Documents/luna, flat, not double-nested. If your machine has ~/Documents/luna/luna from a pre-fix clone, scripts/machine-setup/ubuntu.sh includes a migration step that flattens it.
Terminal window
# Clone (or restore from backup) — flat path, NOT ~/Documents/luna/luna
Hooks: gitleaks (secrets scan) + standard hooks. Luna-specific rules live in ~/Documents/luna/CLAUDE.md (not linked here; it’s in a different repo).
Vault not at ~/Documents/luna, or want to route a repo’s session notes to a different vault? Point the end-session-wiki capture at it; see Choosing the target vault (and §7 below).
5a. Optional — Obsidian Web Clipper templates (Jira LUNA-2)
If you’ll be clipping web pages into Luna (X posts, articles, Reddit threads, newsletters, YouTube videos), install the Obsidian Web Clipper Chrome extension + drop in the 6 pre-built JSON templates that ship with Luna. Skip if you only want to use Luna for native notes.
Extension icon → Settings (gear) → scroll to Templates.
Drag and drop all 6 .json files from ~/Documents/luna/_Templates/Web-Clipper/import/ onto the Templates list. Six templates appear: names, triggers, properties, body all wired up.
Mark General Article as Default template (it has no triggers → fires when no other template matches).
Smoke-test: clip any x.com/... URL → confirm Tweet template fires + note lands in Clippings/.
The drag-and-drop is the entire install. Trigger tables + post-install tuning live in luna at _Templates/Web-Clipper/import/README.md; the why behind each section lives in luna at 30-Resources/Tech/Obsidian Web Clipper Templates.md (both in the luna repo, not linkable from himmel).
Triage of clipped notes (action items / labels / Related Notes hygiene) is implemented as the obsidian-triage plugin shipped from himmel’s marketplace; see §6 below for install. Ticket: LUNA-3; handover tracked in the operator’s private handover repo.
6. Claude Code Plugins
Skip if you skipped §5 (Luna vault). The plugins in this section are either luna-dependent or personal-workflow tools; none are required for core himmel operation. Install only what you need.
Plugins live at ~/.claude/plugins/. Different install methods per plugin: read the Source column carefully:
claude plugin install obsidian-second-brain@himmel after adding himmel marketplace. Never clone it into ~/.claude/skills/: a user-scope copy ignores enabledPlugins and loads into every session
Daily notes, kanban, ADRs, vault operating manual. Opt-in: no named profile enables it; /profile enable does
handover
himmel marketplace
/plugin install after adding himmel marketplace
Handover doc workflows for cross-session continuity
obsidian-triage (LUNA-3)
himmel marketplace
/plugin install after adding himmel marketplace
Autonomous triage of Web Clipper output: /triage-clips + /synthesize-clips. Required only if you set up the Web Clipper templates in §5a
obsidian-markdown, obsidian-bases, json-canvas, obsidian-cli, defuddle. obsidian-triage can use obsidian-markdown for proper OFM when editing clipped notes (recommended, not required — fallback documented in the command body)
Wiki query, save, ingest, lint, autoresearch. Companion to obsidian-triage — wiki-query optionally powers richer Related Notes inference
Two-tier plugin profile: ALWAYS / ON-DEMAND / NOT INSTALLED
docs/setup/settings-template.json splits every plugin’s install/enable state
into three tiers; its own _enabledPluginsComment / _onDemandPluginsComment /
_onDemandConnectorsComment are the authoritative definitions; summarized here:
ALWAYS (enabledPlugins → true): installed AND enabled on every himmel
machine: lean-skills, qmd@himmel, handover@himmel,
himmel-ops@himmel. lean-skills
replaces superpowers + mattpocock-skills (those two fat
upstream plugins carried ~2.2k tok of skill listing into every session for
skills almost never invoked; lean-skills vendors verbatim just the subset
himmel actually uses; see marketplace/plugins/lean-skills/README.md).
plannotator-effective-html (the design/UI kit) is no longer ALWAYS either:
it is design-profile-gated (see the design profile in
scripts/lanes/plugin-profiles.json): invoked once in 1628 local
transcripts, it now opts in per design/UI work instead of paying its ~630
tok cost on every session.
ON-DEMAND (a key of the top-level onDemandPlugins object, also false
in enabledPlugins): INSTALLED by install-plugins.sh but left
DISABLED, reachable in one command, costing no session-start context. Table
toggle command below.
NOT INSTALLED (an enabledPluginsfalse entry absent from
onDemandPlugins): the earlier lean floor, unchanged by the
two-tier profile; the table in the next subsection covers this tier, and re-enabling
one there still means claude plugin install, not just enable.
Design profile: log in to context7 and agent-native-dispatch once per
machine. Both stay on for design work: context7 serves current library
docs, and agent-native-dispatch is the hosted Builder.io MCP server
(https://dispatch.agent-native.com/mcp) that builder-visual@himmel
registers when the design profile enables it. Until each is authenticated, a
design worker cannot answer the OAuth prompt and looks hung. To log in,
make sure the plugin is enabled, start an interactive claude session, run
/mcp, pick each server and choose Authenticate. himmel-doctor check
C72-design-mcp-auth warns when either login is missing (it reads the local
MCP OAuth store, no network call, and stays silent where that store is a
keychain). Never use agent-native-dispatch on salus/PHI work.
Toggling the on-demand tier — plugin-profile.sh / /profile
scripts/machine-setup/plugin-profile.sh is the only
sanctioned writer for an on-demand flip. It writes solely through
claude plugin enable|disable <spec> --scope user, never a hand-edited
settings.json (hook-blocked: block-edit-live-settings). The /profile slash
command wraps it.
Terminal window
bashscripts/machine-setup/plugin-profile.shlist# both tiers + live state + neededBy
bashscripts/machine-setup/plugin-profile.shlean# disable every on-demand plugin that is currently enabled
bashscripts/machine-setup/plugin-profile.shfull# enable installed on-demand plugins at user scope
bashscripts/machine-setup/plugin-profile.shenable<spec># enable one, user scope
bashscripts/machine-setup/plugin-profile.shdisable<spec># disable one, user scope
<spec> takes a bare plugin name (context7) when it is unambiguous across
both tiers, or the full plugin@marketplace form. --dry-run prints the
claude plugin … command instead of running it; --json (list only) emits
the tier table as JSON; --template PATH overrides the template. disable
refuses the three harness-operational plugins handover@himmel,
himmel-ops@himmel, qmd@himmel; disabling any of them would break the
session running the command.
Each write is verified: after claude plugin enable|disable the script re-reads
claude plugin list and confirms the user-scope state, so an exit-0 no-op is
reported rather than acknowledged (project/local state is never read as user
state; those settings may still override the effective state). Exit codes: 0
reached (or already there); 1 a CLI call failed, the state could not be read
or verified, or a bulk lean/full had any write refused (floor) or failed;
2 usage error or a single disable <spec> refused by the floor.
On-demand plugin
Needed by
Enable
context7@claude-plugins-official
/find-docs and the context7-mcp skill — live library/API documentation lookups
plugin-profile.sh enable context7
typescript-lsp@claude-plugins-official
TypeScript/JS work in this checkout (scripts/jira, marketplace/plugins/claude-hud) — go-to-definition and diagnostics
plugin-profile.sh enable typescript-lsp
playwright@claude-plugins-official
Browser-driven e2e work; the /run skill’s browser-driven fallback
plugin-profile.sh enable playwright
claude-obsidian@himmel
The obsidian-vault MCP used by the obsidian-second-brain skill and /obsidian-health — disabling the plugin is what disables that MCP
plugin-profile.sh enable claude-obsidian
luna-correlate@himmel
Cross-vault correlation over luna (HIMMEL_MCP_LUNA_CORRELATE workflows)
plugin-profile.sh enable luna-correlate
obsidian-triage@himmel
/luna-ingest and the luna clip pipeline. The pipeline itself does NOT need it enabled here — see the cadence exception below
plugin-profile.sh enable obsidian-triage
telegram-himmel@himmel
The telegram MCP behind /telegram-clip and the Telegram connector parts — disabling the plugin is what disables that MCP
plugin-profile.sh enable telegram-himmel
codex@openai-codex
The codex worker lanes resolve its cached companion by glob — needs the plugin INSTALLED, never enabled
plugin-profile.sh enable codex
An on-demand enable is a LIVE toggle only: an opt-in reconcile
(HIMMEL_RECONCILE_PLUGINS=1, e.g. via /himmel-update) writes the
template’s enabledPlugins map verbatim and turns it back off; see the
floor-reapply note below, which now applies to this tier too. To keep one on
permanently on this machine, record "<spec>": true in
~/.claude/settings.local.json as himmel reconciliation input. The next
opt-in reconcile copies that value into settings.json; Claude Code does not
read the user sibling as a runtime settings layer.
On-demand CONNECTORS — not installed or managed by himmel
The template’s onDemandConnectors (claude-in-chrome, the claude.ai
Gmail/Drive/Calendar connectors as google-workspace) is discovery-only:
himmel does not install or configure either. claude-in-chrome is a per-site
permission in the Chrome extension; google-workspace is a toggle under
claude.ai → Settings → Connectors. They’re listed in the template purely so
plugin-profile.sh list can name them and the install summary can point at
them.
Cadence exception: force-enabling an on-demand plugin per run
scripts/luna/pipeline-cadence.sh’s emit_settings_fragment force-enables
obsidian-triage@himmel via a --settings fragment
({"enabledPlugins": {"obsidian-triage@himmel": true}}) on every cadence run:
--settings is the highest non-managed precedence layer, so it wins over a
settings.local.json disable and keeps the cadence live even while the
machine’s own profile leaves obsidian-triage on the on-demand tier,
disabled. This is the pattern for any future unattended job that depends on
an on-demand plugin: force-enable it per run via its own settings fragment,
never by flipping the machine’s live profile.
Lean profile — disabled by default, enable on need
This is the NOT INSTALLED tier of the two-tier profile above:
none of the plugins in this table are keys of onDemandPlugins, so
re-enabling one here is a full claude plugin install, not just an
enable. docs/setup/settings-template.json ships lean: 16 re-enableable
@claude-plugins-official plugins (the table below) and obsidian@obsidian-skills
are false in enabledPlugins; every adopter
and every re-provisioned operator machine gets the minimal set by default instead
of re-creating the maximal 31-plugin install every time. (A 17th
@claude-plugins-official entry, pr-review-toolkit, is also false but is not
listed for re-enable: /pr-check dispatches pr-review-toolkit:* agents only
when CR_CLAUDE_AGENTS=1.)
Turn any of these back on with one command
(--scope user shown; swap for project/local per Scope above):
Plugin
Enable command
Note
github@claude-plugins-official
claude plugin install github@claude-plugins-official --scope user
gh CLI over MCP is the standing project rule — enable only for a one-off need
feature-dev@claude-plugins-official
claude plugin install feature-dev@claude-plugins-official --scope user
plugin-dev@claude-plugins-official
claude plugin install plugin-dev@claude-plugins-official --scope user
code-review@claude-plugins-official
claude plugin install code-review@claude-plugins-official --scope user
himmel ships its own pr-review-toolkit(-himmel) critics — enable this only to also run the upstream flow
ralph-loop@claude-plugins-official
claude plugin install ralph-loop@claude-plugins-official --scope user
pyright-lsp@claude-plugins-official
claude plugin install pyright-lsp@claude-plugins-official --scope user
operator convention: don’t flip this on at user scope — dispatch a subagent with the plugin enabled for the one python task instead
agent-sdk-dev@claude-plugins-official
claude plugin install agent-sdk-dev@claude-plugins-official --scope user
claude-code-setup@claude-plugins-official
claude plugin install claude-code-setup@claude-plugins-official --scope user
code-simplifier@claude-plugins-official
claude plugin install code-simplifier@claude-plugins-official --scope user
commit-commands@claude-plugins-official
claude plugin install commit-commands@claude-plugins-official --scope user
playground@claude-plugins-official
claude plugin install playground@claude-plugins-official --scope user
skill-creator@claude-plugins-official
claude plugin install skill-creator@claude-plugins-official --scope user
atlassian@claude-plugins-official
claude plugin install atlassian@claude-plugins-official --scope user
enable-on-need: Jira is CLI-first per project rules (scripts/jira/dist/index.js) — enable only when a session needs interactive Confluence/Atlassian skills. The optional atlassian MCP integration is separate (see Optional integrations)
claude-md-management@claude-plugins-official
claude plugin install claude-md-management@claude-plugins-official --scope user
powers /claude-md-audit: that command degrades to a one-line re-enable hint while this is off; re-enable to run CLAUDE.md audits via the claude-md-improver skill
hookify@claude-plugins-official
claude plugin install hookify@claude-plugins-official --scope user
Scaffold hooks from a prompt — dev-authoring only
security-guidance@claude-plugins-official
claude plugin install security-guidance@claude-plugins-official --scope user
redundant with himmel’s CR gate — see the note below. Enable only if you are running himmel without the critic panel / CodeRabbit
obsidian@obsidian-skills
claude plugin marketplace add kepano/obsidian-skills then claude plugin install obsidian@obsidian-skills --scope user
obsidian-triage falls back to plain markdown when this isn’t enabled (documented in the command body) — enable if you need proper OFM parity
Note:scripts/machine-setup/install-plugins.sh
calls claude plugin install for the union of
enabledPlugins entries flagged true and onDemandPlugins keys (mirrored
false in enabledPlugins). A fresh adopt/re-provision therefore installs
the ALWAYS and ON-DEMAND tiers, not the NOT INSTALLED table above or the
pre-lean 31-plugin default. Use the per-plugin commands above to opt any of
those NOT INSTALLED plugins back in.
Note: the floor is re-applied on every /himmel-update by
scripts/machine-setup/reconcile-enabled-plugins.sh, a whitelist
reconciler, not the additive-only installer above: anything true in your
live enabledPlugins that isn’t true in the template is forced back to
false, so a manual “turn it back on” toggle does not survive the next
update. Preview without writing: bash scripts/machine-setup/reconcile-enabled-plugins.sh --dry-run --scope user;
an already-converged machine prints no drift — already at the lean floor.~/.claude/settings.local.json is the one file reconcile never rewrites;
it is read as an override that wins in both directions: a true there
keeps a personal plugin (e.g. codex@openai-codex) enabled through every
reconcile, a false there drops a floor plugin (e.g. playwright) you
don’t want. Toggling a plugin off with claude plugin disable while its key
is still true in settings.local.json does not stick: the next
reconcile reads the override, not the live claude plugin state, and turns
it back on. Change the override, not just the live toggle. This is the same
mechanism the on-demand tier’s escape hatch above relies on;
it is not floor-specific.
security-guidance@claude-plugins-official runs an LLM diff review at the end
of every turn, an agentic commit review on every git commit, and ~9 hooks
per session (Claude and Codex wiring). himmel already gates the same surface,
harder and later: the /pr-check cross-model critic panel must be clean before
gh pr create (the review-owed marker hook blocks it otherwise), and the CodeRabbit App
review (which can only run once the PR exists) must be clean, with zero
unresolved threads, before merge. Running security-guidance too means paying a
model call per turn for a weaker version of a check those gates already block
on, and adding process spawns to the pool that a kernel-handle
leak was traced to.
Adopter default: OFF.docs/setup/settings-template.json and the user
profile in scripts/lanes/plugin-profiles.json both ship it false as of
this change; no adopter or dispatched lane enables it.
Operator box: decision pending. It is still ENABLED on the operator machine
(user scope, Claude + Codex). The recommendation is to disable it:
But that is a user-scope change the operator applies, so that change
deliberately did not flip it. Re-enable per-session or per-repo instead if
you ever want the per-turn review back.
/obsidian-daily # from obsidian-second-brain, once installed and enabled
/triage-clips --dry-run # from obsidian-triage; should exit 0 with "no Clippings/" or per-clip preview
Note: claude-obsidian is pinned to an immutable tag in himmel’s marketplace/.claude-plugin/marketplace.json per supply-chain policy (a bare commit SHA is not installable). To update it, follow the Pin update workflow in marketplace/plugins/obsidian-triage/README.md. obsidian (kepano) is not in himmel’s marketplace; install it from obsidian@obsidian-skills.
Scope: user vs project
The /plugin install flow above records the plugin at user scope (~/.claude/settings.json), enabled for you across every project. That’s the right default for personal-workflow plugins. The alternative is project scope: declare the marketplace + plugins in a repo’s.claude/settings.json, so anyone who clones that repo gets them auto-known and enabled (each person is still prompted to trust the marketplace on first use).
The setup scripts let you pick: scripts/machine-setup/install-plugins.sh takes --scope user|project|local (default user).
The top-level setup script prompts you to choose. For project/local the target is the current directory, so run from the repo you want the plugins scoped to. The CLI does it directly too: claude plugin install <name>@himmel --scope project writes the block below for you. Same keys, different file:
Pick by intent: yours, everywhere → user scope; part of this project, shared on clone → project scope. Committing extraKnownMarketplaces ships a “trust this third-party registry” into the repo: fine for your own repos, a supply-chain call if it has outside contributors. (The JSON above is the illustrative hand-edit form. himmel’s setup scripts instead read settings-template.json (which registers the marketplace from a local directory source rather than the GitHub repo shown above) and apply the plugin set at whichever --scope you pick.)
Direct install (copy-paste, no setup script)
The Install sequence above runs as part of the machine
setup. To skip setup and just add the marketplace + the plugins you want,
choosing the scope per command, copy-paste instead:
Terminal window
# Register the marketplace (once; the GitHub slug is case-insensitive)
claudepluginmarketplaceaddyotamleo/himmel
# Install plugins — --scope user (default, every project) or
# --scope project (this repo's .claude/settings.json, shared on clone)
The plugins carry their own slash commands + skills; that’s all you need to use
them.
Troubleshooting: Host key verification failed on plugin install
Every @himmel plugin installs from the local marketplace clone except
claude-obsidian, which is sourced from a separate GitHub repo and is the only
one Claude Code must git clone over the network. himmel’s manifest now points
it at an explicit HTTPS url so a fresh machine clones over HTTPS (no SSH
host key needed). If you still hit:
Failed to clone repository: ... No ED25519 host key is known for github.com
and you have requested strict checking. Host key verification failed.
your git is rewriting HTTPS → SSH (a url."git@github.com:".insteadOf "https://github.com/" in ~/.gitconfig), so the clone resolves over SSH on a
box with no github.com entry in ~/.ssh/known_hosts. Pre-seed the host key
once, then retry the install:
Remove (project scope): delete that plugin’s enabledPlugins line from the repo’s .claude/settings.json (and drop the extraKnownMarketplaces.himmel block once no himmel plugin is left).
Move user → project:/plugin uninstall <name>@himmel, then add it to the repo’s .claude/settings.json as above.
Move project → user: remove its enabledPlugins line from the repo settings, then /plugin install <name>@himmel.
7. End-session wiki hook (auto-capture to Luna vault)
Skip if you don’t use the Luna vault; this hook writes session notes there and has no effect without it.
Every Claude Code session is auto-captured to the Luna vault as a structured note under sessions/YYYY/MM/YYYY-MM-DD-HHMM-<repo>-<branch>.md via a SessionEnd hook. Built by epic #7; full schema + opt-out in docs/luna/end-session-wiki.md.
Register it by hand.bash scripts/setup.sh hands off to himmel install, whose wire targets do not include SessionEnd, so add the block to ~/.claude/settings.json yourself (back the file up first):
For per-repo opt-out, env-var disable, dry-run mode, and full operational reference, see docs/luna/end-session-wiki.md.
8. MCP Servers
himmelctl status now reports the graphify-mcp item by checking its
registration in ~/.claude.json (for a focused check:
node scripts/himmelctl/bin.js status --items graphify-mcp).
tokensave — dropped
tokensave was evaluated on 2026-09-06 and dropped: it lost to plain
Grep+Read on all three measured symbol-level ops. Do not install it; audit
reports live in the state repo. The installer no longer provisions it
(removed: the manifest item, install dispatch, and its
minimal/other-profile membership); a fresh install never registers it.
graphify — optional knowledge-graph CLI + MCP server
graphify powers the structure/architecture half of the retrieval routing
(qmd finds content, graphify explains structure; see CLAUDE.md). Opt-in;
install via setup:
Terminal window
bashscripts/setup.sh--with-graphify
(Installs the himmel fork via uv tool install at a pinned commit SHA;
an existing foreign graphify install is adopted as-is, never reinstalled;
see scripts/lib/graphify-bin.sh.)
The install exposes two binaries: graphify (the CLI) and graphify-mcp
(the MCP server). The CLI (graphify query|explain|path, worker-graph.sh) is the
supported path and what agents use. The MCP server is on demand, not
default (HIMMEL-7177): setup.sh --with-graphify (and adopt.sh --with-graphify at user/local scope) write the absolute graphify-mcp path
into the on-demand registry ~/.config/himmel/mcp-on-demand.json (override
HIMMEL_MCP_ONDEMAND_FILE) and never into ~/.claude.json, so a normal
session does not load its 10 tool schemas. To use mcp__graphify__*, generate
the profile and launch with it (node scripts/mcp/build-mcp-profiles.mjs, then
claude --strict-mcp-config --mcp-config .claude/mcp-profiles/local.graph.json).
adopt.sh --scope project still registers the bare name in the committed
.mcp.json (a project adopter’s explicit choice, portable across machines).
To (re-)register on demand by hand:
bash scripts/lib/graphify-bin.sh register-mcp-on-demand. A machine that
registered graphify at user scope before this change keeps loading it by default
until you run claude mcp remove graphify -s user and then the command above.
Claude Code is the only agent graphify needs (no codex/other-CLI prerequisite).
Graph refresh stays lean-invoke (graphify <corpus-copy> --update), never
a hook; extraction backends are governed by the egress matrix; see the
graphify section in CLAUDE.md.
Optional integrations
Skip if you don’t use the Luna vault or Atlassian (Jira/Confluence). Both entries here are optional integrations.
Configure in Claude Code settings (~/.claude/settings.json):
obsidian-vault (on demand, HIMMEL-7176): uvx mcp-obsidian pointing to the Luna vault path. Put its spec in ~/.config/himmel/mcp-on-demand.json ({"mcpServers":{"obsidian-vault":{...}}}, same shape as ~/.claude.json), not in ~/.claude.json, which every session loads. Launch with node scripts/mcp/build-mcp-profiles.mjs, then claude --strict-mcp-config --mcp-config .claude/mcp-profiles/local.vault.json. The Obsidian Local REST API plugin stays installed either way: the end-session-wiki hook writes session notes through it.
atlassian: Jira/Confluence MCP (requires token)
8.6. Telegram bridge onboarding
Skip if you don’t use the Telegram bridge for sending messages to Claude. This is operator-specific infrastructure; it is not required for any core himmel workflow.
scripts/setup.sh step [7/8] runs
scripts/setup/onboard-telegram.sh: scaffold-only, also safe to
run standalone. It:
creates ~/.claude/channels/telegram/ + a TELEGRAM_BOT_TOKEN=.env
template (never overwrites an existing .env);
reports access.json (pairing) status. Setup never writes
access.json: the allowlist is a prompt-injection surface and the live
bridge must be restarted by the operator after edits. Create it yourself:
{"allowFrom":["<your-telegram-user-id>"]};
never starts the bridge (one getUpdates owner per token; a blind
start could 409-conflict a live poller). Bring-up after token + pairing:
bash scripts/telegram/restart-bridge.sh start. Reboot persistence + full ops:
scripts/telegram/README.md /
docs/internals/telegram-bridge.md.
To find the ids for access.json without guessing, run
bun scripts/telegram/onboard.ts [--timeout <seconds>] (default 300s)
before bringing the bridge up: it prints a one-time nonce, waits for that
nonce to come back in a Telegram message, then reports the chat.id /
chat.type / from.id it arrived on: the values you hand-copy in. It is a
getUpdates consumer itself, so it refuses to start while a poller is
live (the same one-owner-per-token rule); stop the bridge first
(bash scripts/telegram/restart-bridge.sh stop, or
bun scripts/telegram/supervisor.ts --kill) if it is already running. Like
the rest of setup it never writes access.json itself. Full walkthrough:
scripts/telegram/README.md.
8.7. Uninstall / offboard
scripts/uninstall.sh is the symmetric teardown of setup +
install-plugins. Run it through the wizard (node scripts/himmelctl/bin.js uninstall), which adds its own one confirm then execs the script below with
--yes (see §4); or invoke the script directly. Destructive and
fail-closed: interactive runs prompt; non-interactive runs abort without
--yes. Preview first:
Terminal window
bashscripts/uninstall.sh--dry-run
himmelctl uninstall --yes confirms without prompting; --dry-run previews
without changing anything. Exit codes: 0 = torn down (or preview complete),
2 = non-interactive refusal without --yes, 3 = operator declined;
otherwise the teardown script’s own code is returned. For uninstall.sh,
2 also means bad usage or an incomplete teardown (any failed step halts
later steps), and 3 means the live-HOME safety fence refused a wet run.
Steps: (1) stop the telegram bun bridge (bun supervisor.ts --kill),
(2) remove telegram pairing + bridge state (~/.claude/channels/telegram/
incl. token + access.json, ~/.claude/handover/bridge/); local delete
does NOT revoke the bot token; revoke via @BotFather when decommissioning,
(3) remove HIMMEL-Resume-* scheduled jobs + the HimmelTelegramBridge
logon task, (4) uninstall each installed Claude plugin at its own scope as
reported by claude plugin list --json, with project-scope rows limited to
the current project; the recorded install profile supplies only a fallback
scope when enumeration is unavailable,
(5) pre-commit uninstall ×3 hook types,
(6) unwire ~/.claude/settings.json: remove env.HIMMEL_REPO,
env.LUNA_VAULT_PATH, and the UNIVERSAL hooks that setup/adopt wired
unconditionally (each helper removes ONLY its own key/stanza; non-himmel keys
(your own hooks, MCP config, the rtk guard) are preserved),
(7) remove the Claude marketplaces (only those no installed plugin still
needs), (8) remove the himmelctl cache + state dir. A provenance ledger,
when install wrote one, decides ownership of the six overwrite-prone rows:
your own pre-existing statusLine, env.HANDOVER_DIR, plugins (4), the
claude-hud config, marketplaces (7) and adopter-script copies are kept with a
printed hand command instead of being guessed at by template; with no
ledger, those same six rows are kept the same way (hand command, never
guessed) while everything else still falls back to template ownership. Partial
offboard via --keep-telegram-state / --skip-plugins / --skip-tasks /
--skip-hooks / --skip-settings (PS: -KeepTelegramState etc.).
--purge-state also removes the provenance ledger (--keep-backups spares
its backup copies). Not touched: the himmel clone + .env, your non-himmel
~/.claude/settings.json keys, handover state outside the bridge root.
9. Verification Checklist
CORE — required for any adopter
rtk --version works
pre-commit run --all-files passes in himmel
Claude Code loads and hooks fire (run any command; check no hook errors appear)
Worktree round-trip: /worktree test/smoke creates a worktree; /clean removes it after merging
OPTIONAL — per integration
graphify:
graphify --version works; a fresh session does NOT list the graphify MCP server (on demand), and ~/.config/himmel/mcp-on-demand.json holds its entry, which a graph-profile session connects
Jira / HIMMEL project:
node scripts/jira/dist/index.js list returns HIMMEL issues
Luna vault:
pre-commit run --all-files passes in Luna vault
/obsidian-daily creates today’s note in Luna
Luna cadence (only if pipeline was armed in the wizard’s cadences question):
node scripts/himmelctl/bin.js status --items cadence-armed,engine-allowlist,luna-sources reports green/n/a, not red
Telegram bridge:
Bridge responds to a test message sent from Telegram
(only if bridge persistence was installed) node scripts/himmelctl/bin.js status --items bridge-persistence,bridge-health reports green/n/a, not degraded
core.hooksPath gate
The machine-setup script (ubuntu.sh) gates the post-clone
flow on git config --get core.hooksPath being either unset OR set to
an existing path inside the himmel working tree. If neither holds, setup
aborts before pre-commit install runs.
Why: the repo on disk was once renamed yotam_internal → himmel,
but the .git/config was copied with the old hard-coded core.hooksPath
absolute string. Git silently skipped every pre-commit and pre-push hook
for an unknown duration (no-push-to-main, npm-audit, npm-licenses,
code-review-before-push, platforms-tested). PR #100 caught
it manually mid-overnight. This gate prevents recurrence.
The same script (scripts/hooks/check-hookspath.sh) is wired in
three other places:
.pre-commit-config.yaml pre-commit stage: every commit fails if
misconfigured.
Claude Code ~/.claude/settings.json SessionStart array: prints a
one-line warning when a session starts on a misconfigured repo.
Non-blocking. The shared docs/setup/settings-template.json does
NOT carry this entry; instead scripts/machine-setup/ubuntu.sh
appends the bash entry.
The smoke test at scripts/hooks/test-check-hookspath.sh covers
eleven cases: unset, set-inside-repo (absolute + relative), set-but-
missing, set-outside-repo, bypass-via-env, outside-any-git-repo,
linked worktree pointing at primary git-common-dir, outside-both-
worktree-and-git-common-dir, Windows-drive-relative, and Windows
mixed-case absolute prefix (case-insensitive NTFS).
Existing machines (not freshly cloned)
The machine-setup script patches ~/.claude/settings.json on every run, so
the SessionStart entries land automatically. If you have an EXISTING
~/.claude/settings.json you don’t want re-patched, copy the
following fragment manually into the SessionStart[0].hooks array
(replacing <himmel-path> with the absolute path to your himmel clone):
Session-sticky: the env var must be set in the shell that launches the
operation; it cannot be injected per-call by Claude. To restore the gate,
unset the variable and re-launch.
The SessionStart hook has NO bypass: it is non-blocking; the printed
warning IS the affordance.