You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The current install script deploys files directly into ~/.local/bin/ (and merges hook entries into ~/.claude/settings.json referencing those paths). This conflates two concerns:
Kit-managed files (deployed from this repo) live in the same namespace as user-installed binaries and other tools' binaries in ~/.local/bin/.
Orphan rot is structural — when a script is removed from the repo (e.g. discord-bot in commit b079782), ./install does not delete the previously-installed copy. The --prune flag exists but is opt-in, not the default. Users carry around dead binaries indefinitely.
fix(install): union-merge hook matcher arrays in merge_settings() #556 (open) — merge_settings() only adds whole event-keys from the template; it cannot merge new matcher entries into an existing SessionStart/etc. array. Upgrading users get partial hook coverage and don't know it.
Both are symptoms of the same root cause: the install layer can't reliably reconcile the on-disk state with the source tree because it doesn't own a discrete namespace.
Adopt a layout where kit-managed files live in a dedicated, fully-owned directory ("Cellar"), and ~/.local/bin/ contains only symlinks pointing into it. Each install wipes and recreates the Cellar, eliminating orphan rot at the structural level.
Wipe the Cellar:rm -rf ~/.claude/scripts/ — orphans die here.
Deploy fresh from scripts/ (recursive) → ~/.claude/scripts/. Preserves directory structure, executable bits, and subtree organization.
Reap stale symlinks in ~/.local/bin/: walk and remove any symlink whose target lies under ~/.claude/scripts/ but no longer points at an existing file. Catches both removed-from-repo cases (e.g. discord-bot) and rename cases.
Recreate symlinks in ~/.local/bin/ for each Cellar entry that should be on PATH.
Settings.json hook paths
Hook commands in config/settings.template.json change from:
~/.local/bin/hooks/nerf/pre-compact.sh
to:
~/.claude/scripts/hooks/nerf/pre-compact.sh
This is a one-time path migration. merge_settings() needs to detect old paths in user settings and rewrite them — not just add missing matchers (subsumes #556).
Granularity question (worth nailing down before implementation)
Two reasonable defaults for the symlink walk:
(A) Symlink everything in scripts/ recursively — preserves current behavior; subdirs like hooks/nerf/ get mirrored at ~/.local/bin/hooks/nerf/.
(B) Symlink only top-level scripts/ entries — subtrees (hooks, vox-providers, internal helpers) stay in the Cellar only. Hooks are called by absolute path from settings.json anyway; having them in ~/.local/bin/hooks/nerf/ is purely vestigial.
Recommendation: (B). Anything that doesn't need to be invoked from a user shell shouldn't be in ~/.local/bin/. Hooks fall in this category.
Changes
install — rewrite the scripts-and-config phase:
Wipe + redeploy ~/.claude/scripts/
Walk + reap stale symlinks under ~/.local/bin/
Recreate symlinks (granularity per recommendation above)
config/settings.template.json — update all ~/.local/bin/hooks/... paths to ~/.claude/scripts/hooks/....
./install --check — surface dangling symlinks and old-path hook commands as drift.
./install --prune — semantics change: no longer needed for orphan rot (Cellar wipe handles that); could be retired or repurposed.
Tests
First run after upgrade: simulated user with old layout — plain ~/.local/bin/<script> files (not symlinks), old hook paths in settings.json. After install: Cellar populated, symlinks replace plain files, hook paths rewritten, .bak backups preserved.
Steady state: subsequent install on already-Cellar-deployed system — Cellar wiped + redeployed, symlinks intact, no drift reported.
Removed-from-repo case: delete a script from scripts/, run install, confirm both Cellar entry AND symlink in ~/.local/bin/ are gone (no --prune needed).
Settings hook-path migration:~/.claude/settings.json with old ~/.local/bin/hooks/... path gets rewritten to ~/.claude/scripts/hooks/.... User-customized hook entries are preserved in .bak.
User-customized script: if ~/.local/bin/<script> is a plain file (not a symlink) with hand edits, back up to .bak before symlink replacement.
Symlinks pointing outside Cellar: unrelated user symlinks in ~/.local/bin/ are NOT touched.
Acceptance Criteria
~/.claude/scripts/ is the canonical install location for all kit-managed scripts; the entire tree is wiped and recreated on every ./install.
~/.local/bin/ contains only symlinks for kit scripts that need PATH discoverability (granularity per recommendation B).
After running ./install against a tree that has removed a script, both the Cellar entry AND the corresponding ~/.local/bin/ symlink are gone — no --prune flag required.
~/.claude/settings.json hook paths get rewritten from ~/.local/bin/hooks/... to ~/.claude/scripts/hooks/... during merge_settings().
Problem
The current
installscript deploys files directly into~/.local/bin/(and merges hook entries into~/.claude/settings.jsonreferencing those paths). This conflates two concerns:~/.local/bin/.discord-botin commit b079782),./installdoes not delete the previously-installed copy. The--pruneflag exists but is opt-in, not the default. Users carry around dead binaries indefinitely.Concrete failure modes recently surfaced:
discord-botscript was removed from the repo back in PR chore(disc): remove discord-bot bash script replaced by disc-server MCP #283, but every install since then has retained a stale~/.local/bin/discord-bot. In one case it became a dangling symlink into a deleted worktree, leading tocommand -v discord-botsucceeding while invocation failed with HTTP 404 from a path-shape mismatch in the proxy chain.merge_settings()only adds whole event-keys from the template; it cannot merge new matcher entries into an existingSessionStart/etc. array. Upgrading users get partial hook coverage and don't know it.Both are symptoms of the same root cause: the install layer can't reliably reconcile the on-disk state with the source tree because it doesn't own a discrete namespace.
Proposal: Cellar + symlink farm (Homebrew/Nix pattern)
Adopt a layout where kit-managed files live in a dedicated, fully-owned directory ("Cellar"), and
~/.local/bin/contains only symlinks pointing into it. Each install wipes and recreates the Cellar, eliminating orphan rot at the structural level.Layout
Install procedure
rm -rf ~/.claude/scripts/— orphans die here.scripts/(recursive) →~/.claude/scripts/. Preserves directory structure, executable bits, and subtree organization.~/.local/bin/: walk and remove any symlink whose target lies under~/.claude/scripts/but no longer points at an existing file. Catches both removed-from-repo cases (e.g. discord-bot) and rename cases.~/.local/bin/for each Cellar entry that should be on PATH.Settings.json hook paths
Hook commands in
config/settings.template.jsonchange from:to:
This is a one-time path migration.
merge_settings()needs to detect old paths in user settings and rewrite them — not just add missing matchers (subsumes #556).Granularity question (worth nailing down before implementation)
Two reasonable defaults for the symlink walk:
hooks/nerf/get mirrored at~/.local/bin/hooks/nerf/.~/.local/bin/hooks/nerf/is purely vestigial.Recommendation: (B). Anything that doesn't need to be invoked from a user shell shouldn't be in
~/.local/bin/. Hooks fall in this category.Changes
install— rewrite the scripts-and-config phase:~/.claude/scripts/~/.local/bin/merge_settings(): rewrite hook command paths from old → new format; also union-merge new matcher entries into existing event arrays (closes fix(install): union-merge hook matcher arrays in merge_settings() #556).config/settings.template.json— update all~/.local/bin/hooks/...paths to~/.claude/scripts/hooks/...../install --check— surface dangling symlinks and old-path hook commands as drift../install --prune— semantics change: no longer needed for orphan rot (Cellar wipe handles that); could be retired or repurposed.Tests
~/.local/bin/<script>files (not symlinks), old hook paths insettings.json. After install: Cellar populated, symlinks replace plain files, hook paths rewritten,.bakbackups preserved.scripts/, run install, confirm both Cellar entry AND symlink in~/.local/bin/are gone (no--pruneneeded).~/.claude/settings.jsonwith old~/.local/bin/hooks/...path gets rewritten to~/.claude/scripts/hooks/.... User-customized hook entries are preserved in.bak.~/.local/bin/<script>is a plain file (not a symlink) with hand edits, back up to.bakbefore symlink replacement.~/.local/bin/are NOT touched.Acceptance Criteria
~/.claude/scripts/is the canonical install location for all kit-managed scripts; the entire tree is wiped and recreated on every./install.~/.local/bin/contains only symlinks for kit scripts that need PATH discoverability (granularity per recommendation B)../installagainst a tree that has removed a script, both the Cellar entry AND the corresponding~/.local/bin/symlink are gone — no--pruneflag required.~/.claude/settings.jsonhook paths get rewritten from~/.local/bin/hooks/...to~/.claude/scripts/hooks/...duringmerge_settings().merge_settings()union-merges new matcher entries into existing event arrays (closes fix(install): union-merge hook matcher arrays in merge_settings() #556).~/.local/bin/<script>(plain files, not symlinks) are backed up to.bakbefore being replaced with a symlink../install --checkreports both stale symlinks and old-path hook entries as drift../scripts/ci/validate.sh122/0).Non-goals
~/.claude/skills/) to the Cellar pattern — they're already in a dedicated namespace.~/.claude/context-crystallizer/) — already in a dedicated namespace.install-remote.shper repo and don't deploy through this script.--prunein the same PR (decoupled cleanup; can come later if it's truly redundant).Dependencies
merge_settings()is the same code path that needs to handle union-merge of matcher arrays; doing both in one pass is cleaner than two sequential rewrites.Related
~/.local/bin/discord-botwas the trigger that exposed this whole thread.SessionStarthad to be hand-applied viajqbecausemerge_settings()couldn't add it. That workaround disappears once this lands.