How-to: self-update
Keep the crm CLI current. See the CLI reference for every flag.
Check for a newer release
crm self-update --check
--json it emits the standard envelope (data.current, data.latest,
data.update_available); if the release server is unreachable it returns a clean
error envelope rather than hanging or crashing.
Upgrade (install-method-aware)
crm self-update
self-update detects how crm was installed and upgrades it the right way:
- Install-script binary (frozen) — downloads the platform archive, verifies
it against the published
SHA256SUMS(the same integrity check the install script uses), and swaps the bundle in place — thecrmlauncher on your PATH keeps working. On Linux it confirms the outcome on the last line —Updated crm 1.2.3 -> 1.2.4., or an already-up-to-date note naming the current version when there was nothing to do. A checksum mismatch or download failure leaves the existing install untouched and exits non-zero. On Windows the swap itself is deferred — see below. - uv tool / pipx — force-reinstalls from the latest release tag
(
uv tool install --force git+https://github.com/Gharib89/crm@vX.Y.Z, or the pipx equivalent).--forceis used because uv/pipx pin the git commit and a plain upgrade can silently no-op. On a terminal it prints the command and asksProceed? [y/N](default No); non-interactively it runs only with--yes. Ifuv/pipxisn't on your PATH, it falls back to printing the command. - editable /
pip install git+…/ unknown — prints the correct git-based upgrade command and never runs anything.
crm self-update --yes # run the uv/pipx reinstall without the prompt (scripts/CI)
On the Windows binary install, PyInstaller's bootloader holds a file inside the
running bundle open for the whole life of the process, so crm.exe can never
replace its own install while it is the one running self-update. So the new
bundle is staged beside the install and a detached copy of the staged crm.exe
is launched to finish the job; the command you ran then exits, prints:
Staged crm 1.2.4. It is applied as crm exits; the next run reports the outcome.
and the finisher takes over: once it sees the parent process gone, it replaces
the bundle's files in place (one file at a time, not by renaming the directory,
because the running crm.exe's images are open files inside it), re-syncs
installed skills/completion, and records the outcome for the next crm command
to report (below). Running self-update again before that finishes reports the
same staged version instead of starting a second swap:
Update to crm 1.2.4 is already staged by an earlier run and is applied as
that crm exits; the next run reports the outcome.
The previous bundle is parked alongside the install as crm.old-<pid>, and the
staged payload the finisher ran from is parked as crm.new-<pid>; both are
cleaned up automatically on a later self-update run, once they are no longer in
use. If the swap can't complete, the files that had moved are put back, so the
existing install is left working, and the error points you at install.ps1 as
the fallback.
Writing inside a program directory tends to wake a virus scanner or the search
indexer, and while one of those holds a file open the move is refused. Each step of
the swap is therefore retried — for up to a couple of seconds per step, so a
self-update on Windows may pause, and a heavily-scanned install can pause for
longer than that overall. If a lock survives a step's whole window,
something is holding the file open persistently rather than for the length of a
scan — the error says so, to distinguish that from the transient case.
Rarely, putting them back can fail too — a file locked in the meantime, say. The
error says so explicitly, and names the crm.old-<pid> directory: with the
install then split across the two, that directory holds the only copy of the
files that moved. Keep it — copy it somewhere safe before you re-run
install.ps1, because a later self-update reaps parked directories.
The next run reports a Windows swap's outcome
Because the finisher runs after self-update has already exited, no command is
still open to print a confirmation or a failure. Instead, the next crm
command — any command, not just self-update — prints a one-off notice once the
finisher has recorded an outcome:
Finished updating crm to 1.2.4.
or, if the swap could not complete:
The last crm update to 1.2.4 could not be applied: <reason>. Your install is
unchanged (still 1.2.3). Details: C:\Users\you\.crm\update.log
The "unchanged" line appears only when the rollback put everything back. In the
rare split-install case above — rollback itself failed — the notice omits it, and
the <reason> text is the one naming the crm.old-<pid> directory to keep and
the install.ps1 recovery.
A skill/completion refresh warning from the finisher, if any, follows on its own
line. The notice prints on a human terminal only (never under --json, and never
when stderr isn't a terminal) and is reported exactly once — reading it deletes
the record. It is not gated by CRM_NO_UPDATE_CHECK or CI: those opt out of
the passive update-available check, not the outcome of an update you already
ran. For the same reason it is not suppressed on a self-update run either — a
second self-update is exactly where you would look for the first one's outcome.
Because that notice is single-shot, the finisher also appends one line per attempt
to ${CRM_HOME:-~/.crm}/update.log — the file the failure notice names. That is
where to look if you scrolled past the notice, or if the update ran under --json
or a non-terminal stderr, where no notice is printed at all:
2026-07-28T09:14:02 ok 1.2.3 -> 1.2.4
2026-07-29T11:02:55 FAILED 1.2.4 -> 1.2.5: Could not replace the installed files
in C:\Users\you\AppData\Local\Programs\crm: [WinError 32] ...
The --json contract
Under --json, a non-frozen self-update emits these data fields:
install_method—frozen|uv-tool|pipx|editable|pip-git|unknown.current,latest,update_available— the version comparison.command— the exact upgrade command string for this install method.executed— whetherself-updateran that command.exit_status— the command's exit code (present only whenexecutedis true).reason— why nothing ran (present whenexecutedis false):up-to-date,manual-install-method,no-tty-without-yes,declined, ortool-not-on-path.
If an attempted uv/pipx upgrade command itself exits non-zero, self-update
emits an ok:false envelope (exit 1) — so a script sees a non-zero process exit
on a failed upgrade — while still carrying the same data fields
(install_method, command, executed:true, exit_status) alongside the
error message, so you can inspect what ran.
--check is unchanged and method-agnostic (data.current, data.latest,
data.update_available).
On the frozen path, a normal in-process swap emits updated: true with
from_version/to_version. A deferred Windows swap (above) emits
updated: false, pending: true, reason: "swap-deferred" (freshly staged) or
reason: "swap-already-staged" (one was already pending), and the same
from_version/to_version — a scripter keying off updated will not mistake a
staged-but-not-yet-applied payload for a completed upgrade. The outcome itself
only ever shows up in the next command's human notice (above); under --json
it is never mixed into an unrelated command's envelope.
Keeping installed skills in sync
Every non---check self-update re-syncs the agent skills you installed with
crm skill install, so the shipped SKILL.md never lags the CLI. It
reads the install registry (${CRM_HOME:-~/.crm}/installed-skills.json) and, for
each recorded destination whose version is stale, re-copies the bundled skill
tree. This fires on both install types — after a frozen bundle swap, and on a
pip/uv install once the upgraded wheel is in place.
The per-destination outcome is reported under data.skills (a list of
{dest, from_version, to_version, status}, status ∈ refreshed | skipped |
pruned | error):
- refreshed — the skill was re-copied to the current version.
- skipped — already current; no copy.
- pruned — the folder was deleted out-of-band, so its registry entry is dropped (the folder is not recreated).
- error — copying that destination failed (e.g. permissions); the entry is kept for a later retry.
A skill-refresh failure never aborts the binary update — the command still
reports ok:true when the upgrade itself succeeded.
On a deferred Windows swap the finisher does this work instead, after the new
bundle is in place — refreshing earlier would leave a new skill tree beside the
old binary. So the deferred envelope carries no data.skills, and a refresh
failure shows up as an extra line under the next run's outcome notice (above).
The passive update notice
On an interactive terminal, crm checks at most once every 24 hours whether a
newer release exists and prints a one-line notice on stderr after a command
finishes — at most once per 24 hours (tracked via notified_at in the cache),
so it does not reprint on every command. A newly discovered version resets that
gate so the new release is surfaced promptly. The probe runs in the background
and never delays a command.
It is silent — and skips the network entirely — in any of these cases:
--jsonoutput mode (machine-readable output is never polluted),- stderr is not a terminal (pipes, redirects, agents),
- the
CIenvironment variable is set, - the
CRM_NO_UPDATE_CHECKenvironment variable is set, - the command being run is
self-updateitself (it owns its own update messaging; the running process still reports the pre-update version, so the notice would otherwise tell you to upgrade to the release you just installed).
Set CRM_NO_UPDATE_CHECK=1 to opt out permanently:
export CRM_NO_UPDATE_CHECK=1
The last check result is cached under ${CRM_HOME:-~/.crm}/update-check.json.