| name | packaging-and-updater |
| description | Use when touching scripts/build-{deb,rpm,pacman}.sh, scripts/lib/package-common.sh, the claude-update-manager Rust crate under updater/, packaging/linux/ templates and maintainer scripts, or the /opt/claude-desktop/update-builder/ rebuild bundle. Enforces the staging-helper / REQUIRED_BUNDLE_FILES dual-update rule, the SemVer policy for the updater crate, the pkexec privilege boundary, and the maintainer-script contract across deb/rpm/pacman. |
Packaging and Updater
When to use
- Editing any
scripts/build-*.sh script or scripts/lib/package-common.sh.
- Modifying the
claude-update-manager Rust crate (updater/).
- Changing
packaging/linux/ templates, maintainer scripts, or the systemd unit.
- Adding or removing files from the rebuild bundle under
/opt/claude-desktop/update-builder/.
- Debugging a stuck updater state or a failed pkexec install.
Not for: DMG-patching changes (shim surface, .vite/build/index.js needles, locale staging). Use porting-claude-dmg-to-linux for those.
Canonical docs
Route here for depth rather than duplicating:
Invariants you must not break
1. Rebuild bundle parity
The updater re-runs the build pipeline from config.builder_bundle_root (default /opt/claude-desktop/update-builder/). That tree is populated at package-build time by stage_update_builder_bundle in scripts/lib/package-common.sh and validated at rebuild time by REQUIRED_BUNDLE_FILES in updater/src/builder.rs.
If you add a file the rebuild needs, update both sides:
scripts/lib/package-common.sh::stage_update_builder_bundle โ copy the file into the bundle.
updater/src/builder.rs::REQUIRED_BUNDLE_FILES (or OPTIONAL_BUNDLE_FILES) โ declare it.
2. SemVer policy for claude-update-manager
Current crate version in updater/Cargo.toml: 0.4.0.
patch โ fixes, docs, maintenance only.
minor โ compatible feature additions (new subcommand, new UpdateStatus variant added as additive).
major โ incompatible CLI, persisted-state, or install-flow changes.
When bumping, update in the same change:
updater/Cargo.toml
README.md current-version banner
AGENTS.md current-version line
CHANGELOG.md (append-only)
3. Legacy state compatibility
state.json uses these serde aliases โ don't break them:
UpdateStatus::BuildingPackage serializes as "building_package"; "building_deb" is accepted on read.
ArtifactPaths.package_path serializes as "deb_path" (kept for backward compat).
When adding new state, either use #[serde(default)] on new fields or accept absence via Option<...>.
4. Privilege boundary
5. Failed installs stay failed
Do not reintroduce pkexec auto-retry on the 15 s reconcile tick. That regression caused "pkexec nag loops" and is guarded by the test app::tests::failed_state_with_existing_deb_stays_failed. Users retry by waiting for the next upstream check or rebuilding a newer package.
6. Interrupted installs recover
When the daemon starts and state.status == Installing, recover_interrupted_install decides:
- If the candidate version equals the installed version โ
Installed.
- Else if the package artifact still exists on disk โ
ReadyToInstall with a note.
- Else โ
Failed with a message that names the missing artifact.
Keep that logic โ tested by app::tests::interrupted_install_*.
7. Removal cleanup across all three formats
| Format | Where | What it does |
|---|
| deb | packaging/linux/claude-update-manager.{prerm,postrm} | walks /run/user/*, runs systemctl --user stop/disable/daemon-reload |
| rpm | packaging/linux/claude-desktop.spec %preun/%postun | same |
| pacman | packaging/linux/claude-desktop.install pre_remove/post_remove | same |
All three degrade gracefully if runuser or systemctl are missing. Keep that symmetry โ the updater also has a safety net (packaged_runtime_removed exits the daemon when the app bundle and the installed package are both gone), but maintainer scripts are the primary path.
Common operations
Adding a new file to the rebuild bundle
- Add the copy in
stage_update_builder_bundle (or the RPM builder's inlined equivalent โ build-rpm.sh does not source package-common.sh).
- Add the path to
REQUIRED_BUNDLE_FILES in updater/src/builder.rs.
- If the file is only needed for one format, add it to
OPTIONAL_BUNDLE_FILES instead (see how build-rpm.sh / build-pacman.sh are handled).
- Bump updater crate to
minor; append CHANGELOG.md.
Adding a new updater CLI subcommand
- Add a variant to
cli::Commands in updater/src/cli.rs.
- Add a branch in
app::run (updater/src/app.rs).
- Add a test under the relevant module.
- Update
docs/reference/cli.md.
- Bump crate to
minor; append CHANGELOG.md.
Adding a new UpdateStatus
- Add the variant in
updater/src/state.rs with #[serde(alias = "...")] if you want a legacy read-compat name.
- Update the lifecycle diagram in
docs/updater-internals.md and docs/reference/cli.md.
- Cover the transitions where the variant is emitted.
Adjusting a packaging install path
Symmetrically update:
scripts/lib/package-common.sh (or the RPM builder for rpm-specific paths).
packaging/linux/claude-desktop.spec %files.
packaging/linux/PKGBUILD.template (if pacman needs a change).
docs/reference/filesystem.md.
Debugging a rebuild failure in the service
systemctl --user status claude-update-manager.service --no-pager
journalctl --user -u claude-update-manager.service -n 200 --no-pager
tail -120 ~/.local/state/claude-update-manager/service.log
sed -n '1,120p' ~/.local/state/claude-update-manager/state.json
ls ~/.cache/claude-update-manager/workspaces/
tail -200 ~/.cache/claude-update-manager/workspaces/<candidate>/logs/install.log
tail -200 ~/.cache/claude-update-manager/workspaces/<candidate>/logs/build-package.log
If the rebuild fails with Required builder bundle path is missing, the packaging step did not ship that file โ check stage_update_builder_bundle and the file's presence under /opt/claude-desktop/update-builder/.
If the rebuild fails with node: command not found, builder::preferred_node_bin_dirs did not find an NVM toolchain. Either install system Node 22+ or make sure $NVM_DIR or ~/.nvm/versions/node/*/bin contains node/npm/npx.
Verifying before commit
bash -n install.sh scripts/build-*.sh scripts/install-deps.sh
cargo check -p claude-update-manager
cargo test -p claude-update-manager
bash tests/scripts_smoke.sh
make package
dpkg-deb -I dist/claude-desktop_*.deb
dpkg-deb -c dist/claude-desktop_*.deb | sed -n '1,40p'
rpm -qpi dist/claude-desktop-*.rpm || true
pacman -Qip dist/claude-desktop-*.pkg.tar.zst || true
Things NOT to do
- Do not ship
target/release/claude-update-manager with the wrong ABI โ always build release via cargo build --release, not via cargo build.
- Do not invert the pkexec binary preference. The installed binary is trusted;
current_exe is only a fallback.
- Do not remove the legacy serde aliases (
"building_deb", deb_path) without bumping major.
- Do not add new packaging paths without updating
docs/reference/filesystem.md โ maintainers rely on that being current.
- Do not commit a
Claude.dmg or a real dmg_url.
- Do not bypass the maintainer-script
systemctl --user stop/disable flow โ orphaned services are a repeated support burden.