stack-update
Apply a pending claude-code-setup update — fetches upstream, shows what changed, three-way-diffs CLAUDE.md against your local edits, and re-runs install.sh. Invoke on "update the stack", "upgrade the
SKILL.mdskills/stack-update/SKILL.md
GitHubStack update
This kit installs by copying files into ~/.claude — once copied, an installed file has
no link back to the repo. This skill is the manual apply step for the update the SessionStart
hook noticed: fetch upstream, show what changed, gate on approval, and (critically) reconcile
CLAUDE.md, which install.sh's default ("auto") mode never overwrites once it exists — a
--claude-md=replace run (not used by this skill's step 6) does.
State dir: ${CLAUDE_HOME:-$HOME/.claude}/.claude-code-setup/ — installed (SHA that
skills, agents, and settings are at), claude-md-installed (SHA whose global/CLAUDE.md the
user actually accepted — written only when they applied the upstream hunks or confirmed a
completed hand-merge, never on a plain skip; if absent, there is no known base — install.sh
only writes it when it copied CLAUDE.md for a user who had none, so anyone with a pre-existing
personal CLAUDE.md has no stamp at all; never fall back to installed, see step 5),
claude-md-skipped (SHA of upstream's global/CLAUDE.md at the point the user last declined to
reconcile it — suppresses re-showing an identical comparison in step 5; never used as a diff
base and never implies acceptance), last-check (epoch of last poll), remote (<installed> <remote> <count> from the last
successful poll — replayed at each session start while installed still matches), notice (the
session-start notice in the user's language, see step 7), disabled (presence
silences the SessionStart check entirely; touch it to opt out).
Repo: https://github.com/TurboKach/claude-code-setup.git, default branch master (not
main).
Procedure
Steps 1–3 are read-only reconnaissance — nothing outside the scratchpad clone is touched, and no state-dir file is written, until the approval gate in step 4 passes.
Read state. Read
installedfrom the state dir — this is the SHA to diff against. Do not modify any state file yet.Clone. Full
git clone(not--depth) of the repo into the session scratchpad. Full history, because step 3 needs<installed>reachable to diff against; the repo is small, so a shallow clone buys nothing here. The clone is read-only reconnaissance — nothing is copied out of it until approval.Summarize what's new, grouped by surface. First check history shape with
git merge-base --is-ancestor <installed> HEAD:- Ancestor (normal case) — derive the summary from
git log --oneline <installed>..HEADandgit diff --stat <installed>..HEAD; raw commit subjects are not the deliverable, a "what changes for me" bullet list is. - Reachable but not an ancestor — upstream rewrote history (force-push). Label the
summary as divergent history, not "what changed since you installed":
<installed>..HEADis a set difference, not a lineage, and would misrepresent it. Base the bullets ongit diff <installed>..HEADinstead (endpoint comparison — stays correct regardless of rewritten history). <installed>empty, unknown, or unreachable at all — say plainly that a change summary is unavailable because there's no base to diff against. Show only the current HEAD SHA and short subject, and the last ~10 commit subjects (git log --oneline -10) as context — and label that list explicitly as not a diff against what the user has installed.
Write the summary in the language you answer the user in — Claude Code's
languagesetting when set, else the conversation's — not the commit subjects' language. Group bullets by the surfaces the user actually has installed, each stated in user-facing terms (not a hash dump):global/CLAUDE.md→ their always-on rules — what changed, in a sentence.skills/*→ which skills are new or changed.agents/*→ which role agents changed.global/rules/*→ which path-scoped user rules are new or changed.hooks/*→ what now happens differently at session start or on tool calls.install.sh/settings.example.json→ what the installer will now do differently, including a changed recommended master (model/modelSettings) — step 6 may ask about it. Omit a group with no changes.
- Ancestor (normal case) — derive the summary from
Approval gate #1 — apply the update at all?
AskUserQuestionwhose question text opens with step 3's summary —What's new since <installed short SHA> (<N> commits):and the bullets, or step 3's divergent-history / no-base label in their place — and ends "Apply this update?", with options Apply / Show the full diff first / Cancel. The summary goes in the question even when the user wrote the commits.- Show the full diff first → show it, then re-ask this same question; it is not itself an answer.
- Cancel → stop here. Nothing is written and the state dir is left exactly as it was —
do not touch
installedorlast-check, so the update notice correctly reappears next session. - Silence, or the question going unanswered, is not approval — never infer Apply from a default or from the user being away.
- Only Apply proceeds past this point.
The
CLAUDE.mdthree-way diff and approval gate #2 — this is the step that justifies the skill. Step 6 below runsinstall.shwith no--claude-mdflag, so its default (--claude-mdunset, i.e. "auto") mode applies: it deliberately never overwrites an existing~/.claude/CLAUDE.md(see theCLAUDE.mdblock ininstall.sh) — it just prints a reminder to merge by hand. So step 6's install refreshes skills and agents but silently skips the single most important file, and a two-way diff can't tell an upstream improvement apart from the user's own customization. Do it properly. Useclaude-md-installedas the base for this diff, notinstalled— it's the SHA the user actually acceptedCLAUDE.mdat, which may laginstalledif a prior run was skipped or handed off for manual merge.claude-md-installedexists (normal case) — three-way diff:git show <claude-md-installed>:global/CLAUDE.md— what the user last accepted.- the clone's
global/CLAUDE.md— what upstream has now. - the live
~/.claude/CLAUDE.md— the user's file, possibly hand-edited. Diff the first two to isolate what actually changed upstream, then show that upstream change against the live file.
claude-md-installedis absent — unknown base, do not fall back toinstalled. This is the common case:install.shonly writesclaude-md-installedwhen it copiesCLAUDE.mdfor a user who had none, so anyone who already had a personalCLAUDE.md(i.e. every existing user) ends up with aninstalledSHA but noclaude-md-installed. Falling back toinstalledwould silently claim they accepted that revision'sglobal/CLAUDE.md, which they never did, and would permanently hide every upstream change made before it. Checkclaude-md-skippedfirst:- Absent, or upstream's
global/CLAUDE.mdhas changed since the SHA it records — say plainly there's no record of whichglobal/CLAUDE.mdrevision they've accepted (either theirCLAUDE.mdpredates the marker, or they've always kept their own), and show a two-way diff instead — the clone's currentglobal/CLAUDE.mdagainst the live~/.claude/CLAUDE.md— labeled explicitly as "upstream's current version vs yours", not as "what changed upstream since you installed". This diff will be noisy if they carry heavy local edits — expected and honest. Proceed to the gate #2 question below. - Present, and upstream's
global/CLAUDE.mdis unchanged since that SHA — nothing new to show. Note in one line that theCLAUDE.mdreconciliation the user previously declined is still outstanding, skip re-rendering the diff, and skip gate #2 for this run. Then a separateAskUserQuestion— approving the bundle in gate #1 is not approval to rewriteCLAUDE.md, it's a distinct decision because the live file may carry local edits — with three choices: apply the upstream hunks (back up the live file first, preserve any local edits that don't conflict) / show the full diff and let the user decide by hand / skipCLAUDE.mdentirely. Never rewrite~/.claude/CLAUDE.mdwithout an explicit answer to this question — silence is not approval.
- Absent, or upstream's
Install. First, the master-model question — asked on every update unless the user already has it or said not to. Ask only when both hold:
- the live
~/.claude/settings.jsondoesn't match the clone'ssettings.example.jsonrecommendation:modeldiffers ignoring a trailing[1m]in any casing — it selects the same model's 1M window — or any mastermodelSettings.<id>.effortLevelthere differs (every entry exceptclaude-sonnet-5-5, the executors' level, whichinstall.shfills in only where none is saved); ~/.claude/.claude-code-setup/master-dont-askis absent, or its content differs frompython3 -c 'import json,sys; e=json.load(open(sys.argv[1])); print(json.dumps({"model": e["model"], "modelSettings": {k: v for k, v in e["modelSettings"].items() if k != "claude-sonnet-5-5"}}, separators=(",", ":"), sort_keys=True))' settings.example.jsonrun in the clone (a changed recommendation asks again).
Then
AskUserQuestion, showing their currentmodeland effort, with options the recommended model and effort, named from the clone'ssettings.example.json(recommended) →--master=recommended/ keep current, ask next time →--master=keep/ don't ask again →--master=dont-ask/ other, the user typesMODEL_IDorMODEL_ID:EFFORT→--master=<typed>. Not asked → no--masterflag.Run
./install.shfrom the clone, with that--masterflag if one was chosen. It already backs up skills and agents to~/.claude/.backup-<stamp>and merges settings.json keys — reuse it, don't reimplement its logic here.- If it exits non-zero: stop — do not write
installedorclaude-md-installed, the run is not complete. Report exactly what was and wasn't applied (skills/agents may be partially updated;CLAUDE.mdmay already have been changed by gate #2 above, before this step ran). Point the user at the restore path:~/.claude/.backup-<stamp>(skills/agents backupinstall.shalready took) and, if gate #2 applied hunks, theCLAUDE.mdbackup it took. Don't invent a rollback — just leave the state files untouched and tell the user where the backups are.
- the live
Update state. Only now write to the state dir, and only on a zero-exit install:
installed— the clone's new HEAD SHA. Afterinstall.shsucceeds, skills, agents, and settings genuinely are at this revision.claude-md-installedandclaude-md-skipped— one rule, unconditional, no per-path exception, applies whether this was the three-way or unknown-base case in step 5:- Gate #2 answered apply the upstream hunks — write
claude-md-installedat the clone's new HEAD SHA, and deleteclaude-md-skippedif present. This is the only answer that stamps acceptance, because it's the only one whereCLAUDE.mdwas actually rewritten. - Gate #2 answered skip or show the full diff and decide by hand, or gate #2 wasn't
asked this run (step 5's suppressed case) — leave
claude-md-installeduntouched (do not write it, even if absent), and writeclaude-md-skippedat the clone's new HEAD SHA. The declined or unconfirmedCLAUDE.mdchange stays pending and resurfaces in the next run's step 5 instead of being silently dropped or falsely marked accepted.
- Gate #2 answered apply the upstream hunks — write
- Delete
last-checkso the next session re-polls fresh instead of trusting the 24h cache. notice— the hook shows this line at session start, before the user has typed anything, so it can't take the language from the conversation. Write one line in the language you answer in, with the placeholders{n}(commit count, or?),{from}and{to}(short SHAs), phrased so any count reads correctly — e.g.New changes: {n} ({from} → {to}) — run /stack-update, translated. When that language is English, deletenoticeinstead; the hook's built-in English wording is used.
Tell the user to restart Claude Code. Skills, agents, hooks, and settings
envare all read at session start — nothing applied by this run is live in the current session until it's restarted.
Opt-out
touch ~/.claude/.claude-code-setup/disabled silences the SessionStart update check
permanently (delete the file to re-enable).