Migrating an existing himmel install
You already have himmel wired somewhere (by an old adopt.sh run, by hand
from a setup guide, or by a himmelctl version older than this one) and you
want to land on the current installer without losing the wiring you have.
- Installing for the first time → install.md.
- Already on
himmelctland just want the newer code → updating.md. - Somewhere in between, or not sure → you are on the right page.
The whole migration posture is one sentence: converge in place with
himmelctl, do not tear down and reinstall. Every path below is additive.
Step 0 — which state are you in
One command tells you, and it never writes anything you have to undo:
node scripts/himmelctl/bin.js statusTwo outcomes matter:
- Exit code 2, printing
himmelctl: no himmelctl install profile found — run himmelctl install first. There is no install record on this machine, sohimmelctlhas nothing to reconcile against yet. You are in state 1 or 2. - A severity-grouped item list (
red/degraded/green/n/a). The record exists; the reds are your drift. You are in state 4, and possibly state 3 as well, which is about your remote, not your wiring.
The install record is two files in ${HIMMELCTL_CACHE_DIR:-~/.claude/himmel}/:
install-profile.json (the answers that drove the install) and state.json
(what the installer believes each target looks like now). Their absence is the
detection signal above, not a fault.
| From-state | Detect | Converge | Rollback |
|---|---|---|---|
1. Wired by the old adopt.sh, or copied by hand | Hook commands present in the target’s .claude/settings.json, but status exits 2 | install (adopts in place), then ensure | Restore the backed-up .claude/settings.json |
| 2. A station set up by hand from the setup guide | Environment and tools are there, no repo wiring, status exits 2 | install --scope user, then ensure | Restore ~/.claude/settings.json |
3. origin still points at the pre-cutover remote | git remote -v names the old host | git remote set-url origin <new-url> + git fetch --tags — see below | Re-point origin back |
| 4. Older hook wiring under a current install record | status lists red items naming hooks | ensure | Restore the backed-up .claude/settings.json |
State 1 — wired by the old adopt.sh, or copied by hand
Your repo’s .claude/settings.json already runs himmel hooks, but nothing on
this machine recorded which items were meant to be there. ensure cannot
help yet: with no install-profile.json it exits 2 by design, because
reconciling against an install profile it had to guess would be reconciling
against fiction.
node scripts/himmelctl/bin.js install --scope projectnode scripts/himmelctl/bin.js ensureinstall is not a re-install here: it writes the record and adopts the wiring
that is already present rather than replacing it. --scope project takes the
shipped adopter-project preset without asking anything, which is what you
want when the goal is simply to record a state that already exists; drop the
flag for the wizard, or pass --from-profile <file> to replay a saved
install-profile file, if your hand-wiring diverged from that preset. The
follow-up ensure is what closes the gap between “recorded” and “actually on
disk”, per item, with a severity for each.
Preview first if you want the plan without the writes: add --dry-run to
either verb.
This state is about your own repo. Inside a himmel checkout, --scope project is refused: that repo’s .claude/settings.json is the source the
installer generates from, not a target. Use bash scripts/setup.sh (the
contributor gates) and then install --scope user; a contributor record
probes the checkout’s project-scope items from user scope.
State 2 — a station set up by hand from the setup guide
Same missing record, different scope: the machine has the environment variables and the tools, but no repo carries the wiring, so you want the harness for every project on this machine.
node scripts/himmelctl/bin.js install --scope usernode scripts/himmelctl/bin.js ensure--scope user loads the shipped, placeholder-free adopter answers from
docs/setup/profiles/ instead of prompting, so this path
is non-interactive. It writes ~/.claude/settings.json; a per-project
.claude/settings.json still wins where one exists, so a repo you later adopt
at project scope is not fighting this.
State 3 — origin still points at the pre-cutover remote
The cutover has landed. yotamleo/Himmel is now the
source of truth, and a release channel has tags to follow.
Detect this state with git remote -v. If origin still names the
pre-cutover host, migrate with a single git remote set-url origin <new-url>
followed by git fetch --tags. Nothing about your hook wiring, your install
record or your scope changes; this state is about where the code comes from,
not what is installed.
State 4 — older hook wiring under a current install record
status prints reds naming hook scripts that no longer exist, or it prints
reds for items that do exist but were never wired into your settings. Both are
the same class of drift, and both converge the same way:
node scripts/himmelctl/bin.js ensureDo not hand-edit .claude/settings.json to chase the reds. ensure
reconciles the wiring against scripts/install/manifest.json item by item; a
hand-edit fixes the symptom you can see and silently diverges from the record,
which is exactly the state you are trying to leave.
If ensure cannot converge something on its own it says so, naming the items:
himmelctl: N item(s) need manual convergence (no automated install path yet): <ids>That line, and only that line, is your cue to consider the heavier path.
Why ensure, not uninstall → install
ensure is additive: it turns items on toward the recorded install profile,
never off, and it never overwrites an item carrying a deliberate recorded
choice. That is what makes it safe to run against a machine whose history you
do not fully know.
uninstall is the opposite by design: it offboards plugins, scheduled jobs,
git hooks and settings wiring in one pass. Reaching for it as a migration step
throws away the very wiring you are trying to preserve, and it takes your
per-item decisions with it.
So: reserve uninstall → install for the case where ensure has printed the
manual-convergence line above and the named items are ones you actually need.
Even then, run node scripts/himmelctl/bin.js uninstall --dry-run first and
read the plan.
Rollback
Back up one file before you start: .claude/settings.json: the project’s,
at project scope, or ~/.claude/settings.json at user scope. It is what the
tool-call hook wiring lives in, so restoring it undoes that wiring at that
scope in one step. It is not the whole rollback story, and the paragraph after
the snippet is precise about where it stops.
cp .claude/settings.json .claude/settings.json.bak # project scopecp ~/.claude/settings.json ~/.claude/settings.json.bak # user scopeBe honest about what that does and does not cover. Restoring the file removes
the wiring for the hooks that file declares, so the tool-call hooks at that
scope stop firing, immediately. Everything else survives it. The git gates
do not live in that file: they are .git/hooks/ scripts, and a commit or a push
will still be gated after the restore. Neither do the settings at the other
scope: a user-scope install keeps wiring every repo on the machine after you
restore a project’s file. Nor does it touch installed marketplace plugins,
scheduled jobs, or the install record in
${HIMMELCTL_CACHE_DIR:-~/.claude/himmel}/. All of that is what
node scripts/himmelctl/bin.js uninstall exists for, and it is the supported
way to leave entirely. So treat the backup as a fast way to undo one scope’s
tool-call wiring, not as a rollback: for a clean slate, run uninstall rather
than deleting things by hand.
Release channels
A migration is a good moment to pick one, but this page does not define them: the channel model, the resolution order, and the exact behaviour when you are ahead of or diverged from a tag all live in updating.md. Two things worth knowing while migrating:
installdefaults a new station’s saved install-profile file tochannel: stable, and never overwrites achannelyou already set.HIMMEL_UPDATE_CHANNELin the environment outranks that recorded value.
State 3 is the caveat: anyone still on the pre-cutover remote has no release
tags to follow until they converge (git remote set-url origin +
git fetch --tags, above).
The statusline’s cached state
The statusline is a renderer (claude-hud) with its own per-user state under
${CLAUDE_CONFIG_DIR:-~/.claude}/plugins/claude-hud/: cached session token
counts, the prompt-cache anchor, the context-window fallback frame, and the
cost ledgers. That state describes the install that wrote it, not the one you
are converging onto. A macOS report of a statusline showing an expired cache
clock, the previous install’s counts and no cost figure at all was exactly
this, carried across from an earlier himmel instance.
Nothing to do about it by hand: whichever path re-wires the statusline
(install, ensure, or himmel-update’s own re-wire) drops that state as
soon as the wiring it writes differs from what was already there: a different
clone path, an older instance’s hud config, or a first wire. A re-run that
changes neither leaves the caches alone, so converging a machine that is
already on this clone costs a live session nothing. The hud rebuilds whatever
it dropped on the next render.
Where to go next
- install.md — the full install guide and the adaptation checklist for a repo that is not himmel.
- updating.md — routine updates, release channels, offboarding.
- configuration.md — every knob and every gate’s off switch, once you are converged.