From a3af74d4c19baff0d5fd0265b0a39030d579d920 Mon Sep 17 00:00:00 2001 From: WilcoLouwerse Date: Fri, 2 Oct 2026 10:48:02 +0200 Subject: [PATCH 1/6] feat(global-settings): say what an update adds, and explain it on request (v2.8.0) The UPDATE REQUIRED notice named the versions but not what changed, so deciding whether to go through the unlock/relock cycle now meant opening the repository. global-settings/CHANGELOG.md holds one sentence per version. It is not installed. When an update is pending, check-settings-version.sh fetches it from the same source as VERSION and shows a "What this update adds" block under the version lines: one sentence for every version after the installed one, newest first, at most five, with the rest counted. A version without an entry is listed as such, and an unreachable CHANGELOG.md is reported without affecting the update. The notice tells the user they can ask for more details. Claude is told to relay the block as is and, only when asked, look up the commit that set VERSION to each listed version, read its message, changed files and pull request, and link that pull request (or the commit). The notice carries the gh api and git log commands for the repository the version came from. Backfilled entries for 2.5.0 to 2.7.2. The README's bump rule now requires a CHANGELOG.md entry with every VERSION bump; the tests fail without one. Tests: 16 new checks in test-check-settings-version.sh (git-fetch and GitHub paths, pending-range selection, cap, missing entry, missing file). Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/claude/global-claude-settings.md | 4 + global-settings/CHANGELOG.md | 37 +++++++ global-settings/README.md | 11 ++ global-settings/VERSION | 2 +- global-settings/check-settings-version.sh | 103 ++++++++++++++++++ .../tests/test-check-settings-version.sh | 50 +++++++++ 6 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 global-settings/CHANGELOG.md diff --git a/docs/claude/global-claude-settings.md b/docs/claude/global-claude-settings.md index 3143ff37a..2dd504f15 100644 --- a/docs/claude/global-claude-settings.md +++ b/docs/claude/global-claude-settings.md @@ -66,9 +66,13 @@ The source name is included so a stale mirror is visible at a glance: `GitHub` f > NEW SESSION — GLOBAL CLAUDE SETTINGS: UPDATE REQUIRED > Installed: v0.1.0 (outdated) | Latest: v1.0.0 (on origin/main) +> What this update adds: v1.0.0 — one sentence from `global-settings/CHANGELOG.md` +> Want to know more? Ask Claude for the details of these changes. > Say "update my global settings to 1.0.0" to apply the update. > Afterwards, relock with (A) full lock, or (B) lock without settings.json (keeps the VSCode model picker working, but the main file loses the kernel lock and is then guarded only by the regex hooks). +Since v2.8.0 the notice says in one sentence per pending version what the update adds. Ask for more and Claude looks up the commits and pull requests behind each version and links them. See [What the update adds](../../global-settings/README.md#what-the-update-adds-v280). + The relock step always offers both options, each with its own command, and says why (B) is there. It is the same choice described under [Troubleshooting → keep `settings.json` unlocked](#model-or-the-model-picker-fails-with-eperm-operation-not-permitted-open-claudesettingsjson). Since v2.5.2 it is part of the update message itself, not a note in the docs. **Files out of sync** (prominently displayed, since v2.7.0): diff --git a/global-settings/CHANGELOG.md b/global-settings/CHANGELOG.md new file mode 100644 index 000000000..b432d1149 --- /dev/null +++ b/global-settings/CHANGELOG.md @@ -0,0 +1,37 @@ +# Global settings changelog + +One entry per released `VERSION`, newest first. The heading is the bare version number. The first line under it is **one sentence** that says what the update adds or fixes, in words a developer understands without opening the code. + +`check-settings-version.sh` shows that sentence in the session-start UPDATE REQUIRED notice for every version between the installed one and the latest (since v2.8.0). The details live in the commit and the pull request. When a user asks for more, Claude looks those up from the version number. This file is not installed into `~/.claude/` and needs no `VERSION` bump of its own, but every `VERSION` bump needs an entry here (see [Bumping the version](README.md#️-bumping-the-version--required-on-every-change)). + +## 2.8.0 + +The update notice now says in one sentence what each pending version adds, and Claude can explain the changes in more detail, with links to the pull requests, when you ask. + +## 2.7.2 + +An unauthorized `git push` is now denied in every form, including `git -C push`, `git -c k=v push` and a push chained after a command Claude was asked to approve. + +## 2.7.1 + +Picking a model or an effort level no longer reports `settings.json` as out of sync, because the keys Claude Code itself writes for that choice are ignored in the file check. + +## 2.7.0 + +The update notice now installs every hook `settings.json` registers instead of a hand-kept list, and a session start checks that the installed files match the canonical copies. + +## 2.6.0 + +Changes to production (`kubectl`, `oc` or `helm` writes against a `*-prod` namespace or context) are now hard-blocked, and text inside commit messages or heredocs no longer counts as a command. + +## 2.5.2 + +The relock step now offers two options, a full lock or a lock without `settings.json`, so the VSCode model picker can keep working. + +## 2.5.1 + +A push phrase typed next to a pasted image is recognised again, and a skill's text can no longer authorize a push by itself. + +## 2.5.0 + +Hand-rolled waiting (CI watch commands, poll loops, long sleeps and idle heartbeats) is now blocked. diff --git a/global-settings/README.md b/global-settings/README.md index 6f6ad05ed..376dd9638 100644 --- a/global-settings/README.md +++ b/global-settings/README.md @@ -16,6 +16,7 @@ Current version: see [`VERSION`](VERSION) | `sound-notify.sh` | `~/.claude/hooks/sound-notify.sh` | Optional notification-sound wrapper. Reads `~/.claude/sound-config.sh` and plays a sound on question / permission / stop events. Silent by default (added in v2.2.0) | | `user-hooks-dispatch.sh` | `~/.claude/hooks/user-hooks-dispatch.sh` | Reads `~/.claude/user-hooks.json` and executes per-user hooks for every Claude Code event. Lets each user register private hooks that survive a settings update (added in v2.4.0) | | `VERSION` | `~/.claude/settings-version` | Installed version tracker (semver) | +| `CHANGELOG.md` | not installed | One sentence per version; shown in the update notice as "What this update adds" (added in v2.8.0) | | `settings-repo-url.example` | `~/.claude/settings-repo-url` | GitHub repo slug for online version checking | | `settings-repo-ref.example` | `~/.claude/settings-repo-ref` | Branch to track (defaults to `main` when absent; the GitHub raw URL uses `raw.githubusercontent.com///`) | | `sound-config.sh.example` | `~/.claude/sound-config.sh` (optional) | Opt-in sound configuration. Only install if you want notification sounds. User-editable — **not** `chattr +i`-locked | @@ -275,6 +276,14 @@ When the installed version already matches the online one, the hook also compare A missing or changed file turns the session-start message into **FILES OUT OF SYNC**. It names the files, the phrase for step 3 is **"repair my global settings"**, and the unlock, contract and relock steps are the same as for an update. Only the affected files are rewritten; `VERSION` is left alone. On the GitHub path the check costs two extra fetches per session (`settings.json`, then every registered hook in one `curl` call); on the git-fetch path it costs none. A canonical file that could not be fetched is reported as not verified, never as fine. +### What the update adds (v2.8.0) + +The UPDATE REQUIRED notice has a **What this update adds** block under the version lines: one sentence per version after the installed one, up to the latest, newest first, taken from [`CHANGELOG.md`](CHANGELOG.md). It shows at most five versions and counts the rest. A version without an entry is listed as such, not left out. If `CHANGELOG.md` cannot be fetched, the block says so, and the update itself is unaffected. On the GitHub path this costs one extra fetch, and only when an update is pending. + +Ask Claude for more details and it looks up the commit that set `VERSION` to each listed version, reads its message, changed files and pull request, and explains the change with a link to that pull request (or the commit when none merged it). The notice gives Claude the `gh api` and `git log` commands for this, so the answer comes from the history, not from the summary sentence. + +The notice is printed by the *installed* hook, so the block first appears when you update **from** v2.8.0 or later to a newer version. + ### The update contract Claude reinstalls each file with `chmod 555` (hooks) or `chmod 444` (`settings-version`) because `block-write-commands.sh` permits only those two modes on protected paths — `chmod 644`, `chmod u+w` and `chattr` are all hard-denied to Claude, by design, so it can never widen its own access. The consequence is the one step 1 covers: read-only mode is *sticky* across updates and only you can clear it. @@ -311,6 +320,8 @@ Before v2.7.2 the push check ran after the `git -C`, `gh`, `curl` and `docker` p Failing to bump the version means users will not be warned to update, and their installed settings will silently fall behind. +**Every bump also adds an entry to [`CHANGELOG.md`](CHANGELOG.md)**: a `## X.Y.Z` heading with one sentence under it that says what the update adds, in words a developer understands without opening the code. The update notice shows that sentence. A missing entry fails the version-check tests. + Changes to `global-settings/README.md` alone do **not** require a bump: nothing installed changes, so a bump would push every developer through the unlock/relock cycle to copy a file they don't have. Documentation under `docs/claude/` is likewise never a trigger. Semver rules: diff --git a/global-settings/VERSION b/global-settings/VERSION index 37c2961c2..834f26295 100644 --- a/global-settings/VERSION +++ b/global-settings/VERSION @@ -1 +1 @@ -2.7.2 +2.8.0 diff --git a/global-settings/check-settings-version.sh b/global-settings/check-settings-version.sh index 5bad6a145..455590f62 100644 --- a/global-settings/check-settings-version.sh +++ b/global-settings/check-settings-version.sh @@ -379,6 +379,66 @@ needs_repair() { return 1 } +# ── What the update adds: one sentence per version from CHANGELOG.md ───────── +# Only for an outdated install. CHANGELOG.md is fetched from the same source as +# VERSION; it is not installed. Each "## X.Y.Z" heading is followed by one +# sentence, and the notice shows that sentence for every version after the +# installed one up to the online one, newest first. An install whose version is +# unreadable gets the online version's entry only. A version without an entry +# says so instead of being skipped, so the list never reads as complete when it +# is not. +CHANGES_MAX=5 +changes_lines=() +changes_fetched=false +changes_more=0 + +# changelog_summary — the first non-empty line under +# "## ", control characters removed and capped at 300 characters. +changelog_summary() { + awk -v v="$2" ' + /^## / { if (found) exit; found = ($2 == v); next } + found && NF { print; exit } + ' "$1" 2>/dev/null | tr -d '\000-\037\177' | cut -c1-300 +} + +if $online_fetch_ok && [ -n "$canon_dir" ] && semver_gt "$online_version" "$installed_version"; then + fetch_canonical "$canon_dir" CHANGELOG.md + _versions=("$online_version") + if [ -s "$canon_dir/CHANGELOG.md" ]; then + changes_fetched=true + if $installed_ok; then + _versions=() + while IFS= read -r _v; do + validate_semver "$_v" || continue + semver_gt "$_v" "$installed_version" || continue + semver_gt "$_v" "$online_version" && continue + _versions+=("$_v") + done < <(awk '/^## / { print $2 }' "$canon_dir/CHANGELOG.md" | sort -t. -k1,1nr -k2,2nr -k3,3nr -u) + # The online version always leads, with or without an entry. + [ "${_versions[0]:-}" = "$online_version" ] || _versions=("$online_version" "${_versions[@]}") + fi + fi + for _v in "${_versions[@]}"; do + if [ ${#changes_lines[@]} -ge "$CHANGES_MAX" ]; then + changes_more=$((changes_more + 1)) + continue + fi + _s="" + $changes_fetched && _s=$(changelog_summary "$canon_dir/CHANGELOG.md" "$_v") + changes_lines+=("v${_v} — ${_s:-(no summary in CHANGELOG.md for this version)}") + done +fi + +# The repository the details are looked up in: the GitHub slug the version came +# from, or the GitHub origin of the local clone on the git-fetch path. +changes_slug="$online_repo_slug" +if [ -z "$changes_slug" ] && [ -n "$git_root" ]; then + changes_slug=$(git -C "$git_root" remote get-url origin 2>/dev/null \ + | sed -nE 's#^(https://|git@|ssh://git@)github\.com[:/]([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)$#\2#p' \ + | sed 's#\.git$##') + validate_repo_slug "$changes_slug" || changes_slug="" +fi + # ── Color: installed ────────────────────────────────────────────────────────── if ! $installed_ok; then ic="${RED}" ii="✗" @@ -607,6 +667,46 @@ emit_claude_instruction() { # $1 = "the update" | "the repair" echo "from the blocks above — do not introduce loops, variables for the repo slug or GitHub URL, or chmod 644/chattr." } +# emit_changes — the "What this update adds" lines, under the version lines. +emit_changes() { + echo " What this update adds:" + if ! $changes_fetched; then + echo " (CHANGELOG.md could not be fetched from origin/${tracking_ref} — ask Claude for the details)" + fi + local l + for l in "${changes_lines[@]}"; do echo " ${l}"; done + [ "$changes_more" -gt 0 ] && echo " … and ${changes_more} earlier version(s), listed in global-settings/CHANGELOG.md" + echo " Want to know more? Ask Claude for the details of these changes." +} + +# emit_changes_instruction — how Claude relays the summary and, on request, +# digs into the commits and pull requests behind it. +emit_changes_instruction() { + echo "WHAT THIS UPDATE ADDS: Show the 'What this update adds' lines from the notice word for word," + echo "directly under the Installed/Latest lines of your warning block, followed by one line telling" + echo "the user they can ask you for more details about these changes. Do not look anything up yet." + echo "If the user asks for more details, look deeper before answering — never expand on the summary" + echo "sentence from memory. For each listed version, find the commit that set global-settings/VERSION" + echo "to it (its subject ends in '(vX.Y.Z)'), read its full message and changed files, and find the" + echo "pull request that merged it:" + if [ -n "$changes_slug" ]; then + echo " gh api 'repos/${changes_slug}/commits?path=global-settings/VERSION&sha=${tracking_ref}&per_page=20' --jq '.[] | .sha[0:8] + \" \" + (.commit.message | split(\"\\n\")[0])'" + echo " gh api 'repos/${changes_slug}/commits/' --jq '.commit.message, (.files[] | .filename)'" + echo " gh api 'repos/${changes_slug}/commits//pulls' --jq '.[] | .html_url + \" \" + .title'" + fi + if [ -n "$REPO_DIR" ]; then + echo " git -C '${REPO_DIR}' log 'origin/${tracking_ref}' --format='%h %s' -- '${rel_base}/VERSION'" + echo " git -C '${REPO_DIR}' show --stat " + fi + echo "Then explain per version what changed, why, and what it means for the user's daily work, and" + if [ -n "$changes_slug" ]; then + echo "link each pull request (https://github.com/${changes_slug}/pull/), or the commit" + echo "(https://github.com/${changes_slug}/commit/) when no pull request merged it." + else + echo "name each commit (short sha and subject); no GitHub repository is known to link to." + fi +} + # ── Stdout context injected into Claude's prompt ────────────────────────────── # Config problems — must be very prominent in chat @@ -638,6 +738,8 @@ if $online_fetch_ok && semver_gt "$online_version" "$installed_version"; then echo " Installed : v${installed_version} ❌ (outdated)" echo " Latest : v${online_version} ✅ (on origin/${tracking_ref})" echo "" + emit_changes + echo "" emit_unlock_steps "the update" "update my global settings to ${online_version}" echo "" emit_contract @@ -645,6 +747,7 @@ if $online_fetch_ok && semver_gt "$online_version" "$installed_version"; then emit_file_blocks settings.json "${managed_hooks[@]}" VERSION echo "" emit_claude_instruction "the update" + emit_changes_instruction echo "==========================================" echo "" diff --git a/global-settings/tests/test-check-settings-version.sh b/global-settings/tests/test-check-settings-version.sh index 47a1f6787..f08d2e43e 100644 --- a/global-settings/tests/test-check-settings-version.sh +++ b/global-settings/tests/test-check-settings-version.sh @@ -11,6 +11,10 @@ # - the update notice lists every hook the canonical settings.json registers, # derived rather than hand-maintained: a hook registered only in # settings.json appears without any change to the hook script itself +# - the update notice shows the CHANGELOG.md sentence of every pending +# version, newest first and capped at five, says when one is missing or +# the file cannot be fetched, and gives Claude the commit/PR lookup; and +# CHANGELOG.md has an entry for the current VERSION # - a matching install is reported up to date, with every file verified # - a missing or changed hook, or a changed settings.json, is reported # FILES OUT OF SYNC with repair blocks for those files only @@ -150,6 +154,36 @@ has 'sudo chattr +i'; check "update: relock steps present" $? has 'mkdir -p ~/.claude/hooks'; check "update: hooks directory is created before the first hook" $? lacks 'OUT OF SYNC'; check "update: no OUT OF SYNC notice alongside UPDATE REQUIRED" $? +# ── 1b. what the update adds: one sentence per version from CHANGELOG.md ───── +# changelog_versions — the version headings of the canonical CHANGELOG.md, +# newest first; summary_of — the sentence under one of them. +changelog_versions() { awk '/^## / { print $2 }' "$CANON_GS/CHANGELOG.md" | sort -t. -k1,1nr -k2,2nr -k3,3nr; } +summary_of() { awk -v v="$1" '/^## / { if (f) exit; f = ($2 == v); next } f && NF { print; exit }' "$CANON_GS/CHANGELOG.md"; } +CUR=$(cat "$SRC_DIR/VERSION") +[[ -n "$(summary_of "$CUR")" ]]; check "changelog: CHANGELOG.md has a one-sentence entry for VERSION $CUR" $? +eq "$(changelog_versions | head -1)" "$CUR"; check "changelog: the newest CHANGELOG.md entry is the current VERSION" $? +mapfile -t CL_VERSIONS < <(changelog_versions) +has 'What this update adds:'; check "changes: block present in the update notice" $? +has "v${CUR} — $(summary_of "$CUR")"; check "changes: the latest version's sentence is shown" $? +has 'Ask Claude for the details of these changes'; check "changes: the user is told they can ask for more" $? +has 'WHAT THIS UPDATE ADDS:'; check "changes: Claude is told how to relay and dig deeper" $? +has "log 'origin/main' --format='%h %s' -- 'global-settings/VERSION'"; check "changes: git-fetch path gives the local log command" $? +if [[ ${#CL_VERSIONS[@]} -gt 5 ]]; then + has "… and $(( ${#CL_VERSIONS[@]} - 5 )) earlier version(s)"; check "changes: more than five versions are counted, not listed" $? + lacks "v${CL_VERSIONS[5]} — "; check "changes: the sixth version is not listed" $? +fi +_order=$(grep -n -e "^ v${CL_VERSIONS[0]} — " -e "^ v${CL_VERSIONS[1]} — " <<<"$OUT" | cut -d: -f1 | tr '\n' ' ') +[[ "$_order" =~ ^([0-9]+)\ ([0-9]+)\ $ ]] && (( BASH_REMATCH[1] < BASH_REMATCH[2] )) +check "changes: newest version first" $? + +# installed = third-newest entry: the two newer versions are shown, it is not +echo "${CL_VERSIONS[2]}" > "$CLAUDE_DIR/settings-version" +run_hook +has "v${CL_VERSIONS[0]} — "; check "changes: newest pending version listed" $? +has "v${CL_VERSIONS[1]} — "; check "changes: second pending version listed" $? +lacks "v${CL_VERSIONS[2]} — "; check "changes: the installed version is not listed" $? +lacks 'earlier version(s)'; check "changes: no overflow line for two versions" $? + # ── 2. the list follows settings.json: register a new hook upstream ────────── printf '#!/bin/bash\nexit 0\n' > "$CANON_GS/extra-guard.sh" jq '.hooks.PreToolUse += [{"matcher":"Bash","hooks":[{"type":"command","command":"bash ~/.claude/hooks/extra-guard.sh"}]}]' \ @@ -161,6 +195,8 @@ git -C "$CANON" commit -q -m "register extra-guard.sh" run_hook has 'UPDATE REQUIRED'; check "derived: a bumped canonical VERSION triggers the update notice" $? block_for extra-guard.sh; check "derived: a hook registered only in settings.json appears in the update list" $? +has 'v9.9.9 — (no summary in CHANGELOG.md for this version)'; check "changes: a version without an entry is listed as such" $? +has "v${CUR} — $(summary_of "$CUR")"; check "changes: older entries still shown when the latest has none" $? git -C "$CANON" revert --no-edit HEAD >/dev/null fixture_restored; check "derived: canonical repo restored for the remaining scenarios (fixture sanity)" $? @@ -171,6 +207,7 @@ has 'Settings are up to date'; check "clean: reported up to date" $? has 'installed files match the canonical copies'; check "clean: files reported as verified" $? lacks 'OUT OF SYNC'; check "clean: no OUT OF SYNC notice" $? lacks 'UPDATE REQUIRED'; check "clean: no UPDATE REQUIRED notice" $? +lacks 'What this update adds'; check "clean: no changes block" $? grep -q 'verified ✓' <<<"$ERR"; check "clean: panel shows files verified" $? # ── 4. a registered hook is missing ────────────────────────────────────────── @@ -288,6 +325,19 @@ done block_for VERSION; check "raw update: block for VERSION" $? eq "$(last_fetched)" VERSION; check "raw update: VERSION block is emitted last" $? has "printf '%s\\n' \"\$content\" >"; check "raw update: content is written with printf, not curl -o" $? +has "v${CUR} — $(summary_of "$CUR")"; check "raw changes: the latest version's sentence is shown" $? +has "gh api 'repos/ConductionNL/.github/commits?path=global-settings/VERSION&sha=main"; check "raw changes: Claude gets the commit lookup for the repo" $? +has 'https://github.com/ConductionNL/.github/pull/'; check "raw changes: Claude is told to link the pull request" $? +lacks 'CHANGELOG.md could not be fetched'; check "raw changes: no fetch warning when it was fetched" $? + +# R1b. CHANGELOG.md unreachable: the update still works and the block says so +rm "$TMP/shimroot/CHANGELOG.md" +run_hook +has 'UPDATE REQUIRED'; check "raw no-changelog: update notice still present" $? +block_for VERSION; check "raw no-changelog: VERSION block still emitted" $? +has 'CHANGELOG.md could not be fetched'; check "raw no-changelog: the block says the summary is missing" $? +has "v${CUR} — (no summary in CHANGELOG.md for this version)"; check "raw no-changelog: the latest version is still named" $? +cp "$CANON_GS/CHANGELOG.md" "$TMP/shimroot/CHANGELOG.md" # R2. matching install: up to date via GitHub, every file verified install_all From 338d4061b93e29436a139f38b3d3857048a3f9af Mon Sep 17 00:00:00 2001 From: WilcoLouwerse Date: Fri, 2 Oct 2026 11:28:33 +0200 Subject: [PATCH 2/6] feat(global-settings): short update notice at session start, full changelog The UPDATE REQUIRED notice comes back in every new session until the update is done, and Claude rendered it in full each time: the unlock commands, the phrase, both relock options with their trade-off. It now shows a short form at session start: header, installed and latest version, what the update adds, a one-line reminder that unlocking takes sudo chattr -i plus chmod u+w and relocking sudo chattr +i, and that the user can ask how to update or for the details of the changes. Asking how to update brings the full steps 1 to 4 with every command in its own block. After the update Claude always shows the relock step with both options, whether or not the full form was shown. The relock rules are unchanged and now shared between the update and repair notices; the FILES OUT OF SYNC notice keeps its full form. CHANGELOG.md now covers every released version, 1.0.0 to 2.8.0. 1.0.0 to 1.3.0 come from the archived claude-code-config repository; branch-only numbers (1.5.0, 1.5.2, 1.5.3, 2.4.3) were never released and have no entry. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/claude/global-claude-settings.md | 15 ++- global-settings/CHANGELOG.md | 100 +++++++++++++++++- global-settings/README.md | 6 +- global-settings/check-settings-version.sh | 50 +++++++-- .../tests/test-check-settings-version.sh | 10 +- 5 files changed, 157 insertions(+), 24 deletions(-) diff --git a/docs/claude/global-claude-settings.md b/docs/claude/global-claude-settings.md index 2dd504f15..786cb2c3d 100644 --- a/docs/claude/global-claude-settings.md +++ b/docs/claude/global-claude-settings.md @@ -62,16 +62,15 @@ In addition to the terminal panel, the hook always injects a message into Claude The source name is included so a stale mirror is visible at a glance: `GitHub` for the raw-URL method; for the git-fetch fallback the host is inferred from the local clone's `origin` URL (`GitHub` / `GitLab` / `Codeberg` / `git origin`). -**Update required** (prominently displayed, cannot be missed): +**Update required** (prominently displayed, cannot be missed; short since v2.8.0): -> NEW SESSION — GLOBAL CLAUDE SETTINGS: UPDATE REQUIRED -> Installed: v0.1.0 (outdated) | Latest: v1.0.0 (on origin/main) -> What this update adds: v1.0.0 — one sentence from `global-settings/CHANGELOG.md` -> Want to know more? Ask Claude for the details of these changes. -> Say "update my global settings to 1.0.0" to apply the update. -> Afterwards, relock with (A) full lock, or (B) lock without settings.json (keeps the VSCode model picker working, but the main file loses the kernel lock and is then guarded only by the regex hooks). +> GLOBAL CLAUDE SETTINGS: UPDATE REQUIRED +> Installed: v2.7.2 (outdated). Latest: v2.8.0 (on origin/main) +> What this update adds: v2.8.0 — one sentence from `global-settings/CHANGELOG.md` +> Unlocking takes two commands, `sudo chattr -i` and `chmod u+w`. Afterwards you must re-apply `sudo chattr +i`. +> Ask me how to update for the full steps, or for more details about what these changes do. -Since v2.8.0 the notice says in one sentence per pending version what the update adds. Ask for more and Claude looks up the commits and pull requests behind each version and links them. See [What the update adds](../../global-settings/README.md#what-the-update-adds-v280). +The notice returns in every new session until you update, so it is short by default. Ask how to update and Claude shows the full steps: unlock, verify, the phrase to say ("update my global settings to 2.8.0"), and the relock with (A) full lock or (B) lock without settings.json (keeps the VSCode model picker working, but the main file loses the kernel lock and is then guarded only by the regex hooks). Ask about the changes and Claude looks up the commits and pull requests behind each version and links them. See [A short notice, and what the update adds](../../global-settings/README.md#a-short-notice-and-what-the-update-adds-v280). The relock step always offers both options, each with its own command, and says why (B) is there. It is the same choice described under [Troubleshooting → keep `settings.json` unlocked](#model-or-the-model-picker-fails-with-eperm-operation-not-permitted-open-claudesettingsjson). Since v2.5.2 it is part of the update message itself, not a note in the docs. diff --git a/global-settings/CHANGELOG.md b/global-settings/CHANGELOG.md index b432d1149..fac54cfa1 100644 --- a/global-settings/CHANGELOG.md +++ b/global-settings/CHANGELOG.md @@ -4,9 +4,11 @@ One entry per released `VERSION`, newest first. The heading is the bare version `check-settings-version.sh` shows that sentence in the session-start UPDATE REQUIRED notice for every version between the installed one and the latest (since v2.8.0). The details live in the commit and the pull request. When a user asks for more, Claude looks those up from the version number. This file is not installed into `~/.claude/` and needs no `VERSION` bump of its own, but every `VERSION` bump needs an entry here (see [Bumping the version](README.md#️-bumping-the-version--required-on-every-change)). +Versions 1.0.0 to 1.3.0 were released from the now archived `ConductionNL/claude-code-config` repository. Numbers that only ever existed on a branch (1.5.0, 1.5.2, 1.5.3, 2.4.3) were never released and have no entry. + ## 2.8.0 -The update notice now says in one sentence what each pending version adds, and Claude can explain the changes in more detail, with links to the pull requests, when you ask. +The update notice is now short at session start and says in one sentence what each pending version adds. Ask Claude how to update for the full steps, or for the details of the changes with links to the pull requests. ## 2.7.2 @@ -35,3 +37,99 @@ A push phrase typed next to a pasted image is recognised again, and a skill's te ## 2.5.0 Hand-rolled waiting (CI watch commands, poll loops, long sleeps and idle heartbeats) is now blocked. + +## 2.4.6 + +The unlock step now includes the `chmod u+w` that every update after the first needs, so an update no longer stalls halfway with `Permission denied`. + +## 2.4.5 + +Seven `Write(...)` deny rules that Claude Code never applied are removed; the `Edit(...)` rules next to them already cover every file-editing tool. + +## 2.4.4 + +The in-place edit guard no longer hard-denies a chain of harmless commands because the tool, its `-i` flag and the protected path came from different commands. + +## 2.4.2 + +The update notice now also installs `sound-notify.sh` and `user-hooks-dispatch.sh`, which it had left out since they were added. + +## 2.4.1 + +The `mcpServers` block is removed from `settings.json`, because Claude Code never read it there and the browsers it listed never loaded. + +## 2.4.0 + +You can register your own hooks in `~/.claude/user-hooks.json`, and they survive every settings update. + +## 2.3.1 + +The "up to date" message names its source ("via GitHub") again. + +## 2.3.0 + +The canonical source moves back from Codeberg to GitHub (`ConductionNL/.github`), for the version check and for the update itself. + +## 2.2.2 + +The permission-request sound now plays, because it is wired to the event Claude Code actually fires for a permission dialog. + +## 2.2.1 + +Short notification sounds are no longer cut off when the hook exits. + +## 2.2.0 + +Optional notification sounds when Claude asks a question, needs a permission or finishes, off by default and set in `~/.claude/sound-config.sh`. + +## 2.1.0 + +`--legacy-peer-deps` is hard-denied for npm, pnpm, yarn and bun installs. + +## 2.0.0 + +Breaking: the canonical source moves from GitHub to Codeberg (`codeberg.org/Conduction/.github`), so the version check and the update pull from there. + +## 1.7.0 + +Claude's config files get a kernel-level lock (`chattr +i`), and a new hook stops the Write and Edit tools from changing them. + +## 1.6.1 + +A push phrase now stays valid after Claude has run other tools in the same turn. + +## 1.6.0 + +Tighter guards on writes to `~/.claude/`, `settings-repo-path` is no longer unlocked during an update, and the guard hook gets a test suite. + +## 1.5.1 + +You can track a branch other than `main` with `~/.claude/settings-repo-ref`, and destructive commands such as `sudo`, `rm -rf /`, `git reset --hard` and `gh pr merge` are denied. + +## 1.4.0 + +The version check reads the latest version straight from GitHub (`~/.claude/settings-repo-url`), with the local clone as fallback. + +## 1.3.0 + +Write requests through `curl` are caught in more forms, including combined flags such as `-sX POST` and a `curl` behind a pipe. + +## 1.2.2 + +`gh pr`, `gh issue`, `gh repo`, `gh release`, `gh workflow` and `gh run` write commands now ask for approval, also when chained after a read. + +## 1.2.1 + +More read-only commands run without a prompt, and any output redirect to a file now asks for approval. + +## 1.2.0 + +Claude can no longer change its own settings files or make them writable, and the version check retries once when the remote is briefly unreachable. + +## 1.1.0 + +Commands that would leave the WSL workspace (Windows drives under `/mnt//`, `.exe` programs, `wsl`) are hard-blocked. + +## 1.0.0 + +The first mandatory, versioned global settings: a shared `settings.json`, the write-guard hook and a session-start version check. diff --git a/global-settings/README.md b/global-settings/README.md index 376dd9638..5be38f174 100644 --- a/global-settings/README.md +++ b/global-settings/README.md @@ -276,13 +276,15 @@ When the installed version already matches the online one, the hook also compare A missing or changed file turns the session-start message into **FILES OUT OF SYNC**. It names the files, the phrase for step 3 is **"repair my global settings"**, and the unlock, contract and relock steps are the same as for an update. Only the affected files are rewritten; `VERSION` is left alone. On the GitHub path the check costs two extra fetches per session (`settings.json`, then every registered hook in one `curl` call); on the git-fetch path it costs none. A canonical file that could not be fetched is reported as not verified, never as fine. -### What the update adds (v2.8.0) +### A short notice, and what the update adds (v2.8.0) + +The update notice comes back in every new session until you update, so Claude shows it **short** at session start: the header, the installed and latest version, what the update adds, a reminder that unlocking takes two commands (`sudo chattr -i` and `chmod u+w`) and relocking one (`sudo chattr +i`), and that you can ask for more. Ask Claude how to update and you get the full steps 1 to 4 above, with every command in its own block and both relock options. After the update Claude always shows the relock step, whether or not you asked for the full steps. The FILES OUT OF SYNC notice is still shown in full. The UPDATE REQUIRED notice has a **What this update adds** block under the version lines: one sentence per version after the installed one, up to the latest, newest first, taken from [`CHANGELOG.md`](CHANGELOG.md). It shows at most five versions and counts the rest. A version without an entry is listed as such, not left out. If `CHANGELOG.md` cannot be fetched, the block says so, and the update itself is unaffected. On the GitHub path this costs one extra fetch, and only when an update is pending. Ask Claude for more details and it looks up the commit that set `VERSION` to each listed version, reads its message, changed files and pull request, and explains the change with a link to that pull request (or the commit when none merged it). The notice gives Claude the `gh api` and `git log` commands for this, so the answer comes from the history, not from the summary sentence. -The notice is printed by the *installed* hook, so the block first appears when you update **from** v2.8.0 or later to a newer version. +The notice is printed by the *installed* hook, so the short form and the block first appear when you update **from** v2.8.0 or later to a newer version. ### The update contract diff --git a/global-settings/check-settings-version.sh b/global-settings/check-settings-version.sh index 455590f62..42e67c348 100644 --- a/global-settings/check-settings-version.sh +++ b/global-settings/check-settings-version.sh @@ -651,6 +651,37 @@ emit_claude_instruction() { # $1 = "the update" | "the repair" echo "started and that you checked the Global Claude Settings. Then immediately display the" echo "notice as a clearly visible warning block — bold, a header, or a highlighted quote block." echo "The user must not be able to overlook it. Do not bury it in the middle of your response." + emit_full_form_rules "$1" +} + +# emit_update_instruction — the update notice is shown short at session start +# (it returns in every new session until the update is done) and in full only +# when the user asks how to update. The relock options are never skipped: they +# come with the full form and again after the update. +emit_update_instruction() { + echo "INSTRUCTION FOR CLAUDE: At the very start of your response, mention that a new session has" + echo "started and that you checked the Global Claude Settings. Then show the SHORT form of this" + echo "notice as one clearly visible warning block — a header and a few lines, no command blocks:" + echo " - the header 'GLOBAL CLAUDE SETTINGS: UPDATE REQUIRED';" + echo " - the Installed and Latest lines;" + echo " - the 'What this update adds' lines, word for word;" + echo " - one line: unlocking takes TWO commands, 'sudo chattr -i' and 'chmod u+w', and afterwards" + echo " the user must re-apply 'sudo chattr +i', or the immutable lock stays off until the next" + echo " session;" + echo " - one line: the user can ask you how to update for the full steps, or for more details" + echo " about what these changes do." + echo "The user must not be able to overlook the block. Leave out steps 1 to 4, the command blocks" + echo "and the relock options at session start, and do not look anything up yet." + echo "FULL FORM: when the user asks how to update, for the update steps or for more information" + echo "about updating, display steps 1 to 4 of the notice with each command as its own fenced block." + echo "The same rules then apply as after the update below." + echo "AFTER THE UPDATE (the user said the step-3 phrase and you ran the file blocks): always show" + echo "the relock step, even if the full form was never shown." + emit_full_form_rules "the update" +} + +# emit_full_form_rules — what the full notice and the update itself must cover. +emit_full_form_rules() { # $1 = "the update" | "the repair" echo "Also remind the user that the unlock is TWO commands — 'sudo chattr -i' for the kernel flag" echo "and a plain 'chmod u+w' for the read-only mode a previous update left behind — and that they" echo "must run 'sudo chattr +i' afterward to re-apply the kernel-level lock. Without the relock the" @@ -676,19 +707,16 @@ emit_changes() { local l for l in "${changes_lines[@]}"; do echo " ${l}"; done [ "$changes_more" -gt 0 ] && echo " … and ${changes_more} earlier version(s), listed in global-settings/CHANGELOG.md" - echo " Want to know more? Ask Claude for the details of these changes." + echo " Want to know more? Ask Claude how to update, or for the details of these changes." } -# emit_changes_instruction — how Claude relays the summary and, on request, -# digs into the commits and pull requests behind it. +# emit_changes_instruction — how Claude, on request, digs into the commits and +# pull requests behind the "What this update adds" lines. emit_changes_instruction() { - echo "WHAT THIS UPDATE ADDS: Show the 'What this update adds' lines from the notice word for word," - echo "directly under the Installed/Latest lines of your warning block, followed by one line telling" - echo "the user they can ask you for more details about these changes. Do not look anything up yet." - echo "If the user asks for more details, look deeper before answering — never expand on the summary" - echo "sentence from memory. For each listed version, find the commit that set global-settings/VERSION" - echo "to it (its subject ends in '(vX.Y.Z)'), read its full message and changed files, and find the" - echo "pull request that merged it:" + echo "MORE DETAILS ABOUT THE CHANGES: when the user asks what these changes do, look deeper before" + echo "answering — never expand on the summary sentence from memory. For each listed version, find" + echo "the commit that set global-settings/VERSION to it (its subject usually names the version)," + echo "read its full message and changed files, and find the pull request that merged it:" if [ -n "$changes_slug" ]; then echo " gh api 'repos/${changes_slug}/commits?path=global-settings/VERSION&sha=${tracking_ref}&per_page=20' --jq '.[] | .sha[0:8] + \" \" + (.commit.message | split(\"\\n\")[0])'" echo " gh api 'repos/${changes_slug}/commits/' --jq '.commit.message, (.files[] | .filename)'" @@ -746,7 +774,7 @@ if $online_fetch_ok && semver_gt "$online_version" "$installed_version"; then echo "" emit_file_blocks settings.json "${managed_hooks[@]}" VERSION echo "" - emit_claude_instruction "the update" + emit_update_instruction emit_changes_instruction echo "==========================================" echo "" diff --git a/global-settings/tests/test-check-settings-version.sh b/global-settings/tests/test-check-settings-version.sh index f08d2e43e..e5181bc38 100644 --- a/global-settings/tests/test-check-settings-version.sh +++ b/global-settings/tests/test-check-settings-version.sh @@ -165,8 +165,13 @@ eq "$(changelog_versions | head -1)" "$CUR"; check "changelog: the newest CHANGE mapfile -t CL_VERSIONS < <(changelog_versions) has 'What this update adds:'; check "changes: block present in the update notice" $? has "v${CUR} — $(summary_of "$CUR")"; check "changes: the latest version's sentence is shown" $? -has 'Ask Claude for the details of these changes'; check "changes: the user is told they can ask for more" $? -has 'WHAT THIS UPDATE ADDS:'; check "changes: Claude is told how to relay and dig deeper" $? +has 'Ask Claude how to update, or for the details of these changes'; check "changes: the user is told they can ask for more" $? +has 'MORE DETAILS ABOUT THE CHANGES:'; check "changes: Claude is told how to dig deeper on request" $? +has 'show the SHORT form of this'; check "short: the update notice is shown short at session start" $? +has 'ask you how to update for the full steps'; check "short: the short form offers the full steps on request" $? +has 'FULL FORM: when the user asks how to update'; check "short: the full form is shown when asked" $? +has 'AFTER THE UPDATE'; check "short: the relock step is shown after the update" $? +has 'Show BOTH relock options from step 4'; check "short: the relock rules still travel with the notice" $? has "log 'origin/main' --format='%h %s' -- 'global-settings/VERSION'"; check "changes: git-fetch path gives the local log command" $? if [[ ${#CL_VERSIONS[@]} -gt 5 ]]; then has "… and $(( ${#CL_VERSIONS[@]} - 5 )) earlier version(s)"; check "changes: more than five versions are counted, not listed" $? @@ -221,6 +226,7 @@ lacks "origin/main:global-settings/sound-notify.sh"; check "missing: no block fo lacks "origin/main:global-settings/settings.json"; check "missing: no block for an intact settings.json" $? lacks "origin/main:global-settings/VERSION"; check "missing: VERSION is not rewritten" $? has 'Then say: "repair my global settings"'; check "missing: repair phrase present" $? +lacks 'SHORT form'; check "missing: the repair notice keeps its full form" $? has 'sudo chattr -i'; check "missing: unlock steps present" $? lacks 'Settings are up to date'; check "missing: not reported up to date" $? grep -q '1 missing, 0 changed' <<<"$ERR"; check "missing: panel counts it" $? From 02fafed996fd9b982957d0730d949c5bbd2edd4c Mon Sep 17 00:00:00 2001 From: WilcoLouwerse Date: Fri, 2 Oct 2026 11:39:38 +0200 Subject: [PATCH 3/6] fix(global-settings): clear the two ShellCheck findings in the update-summary change SC2028 on the gh api lookup line (printf instead of echo, same output) and SC2319 on a $? that followed a [[ ]] condition in the tests. Co-Authored-By: Claude Opus 5.5 (1M context) --- global-settings/check-settings-version.sh | 2 +- global-settings/tests/test-check-settings-version.sh | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/global-settings/check-settings-version.sh b/global-settings/check-settings-version.sh index 42e67c348..b8c480992 100644 --- a/global-settings/check-settings-version.sh +++ b/global-settings/check-settings-version.sh @@ -718,7 +718,7 @@ emit_changes_instruction() { echo "the commit that set global-settings/VERSION to it (its subject usually names the version)," echo "read its full message and changed files, and find the pull request that merged it:" if [ -n "$changes_slug" ]; then - echo " gh api 'repos/${changes_slug}/commits?path=global-settings/VERSION&sha=${tracking_ref}&per_page=20' --jq '.[] | .sha[0:8] + \" \" + (.commit.message | split(\"\\n\")[0])'" + printf '%s\n' " gh api 'repos/${changes_slug}/commits?path=global-settings/VERSION&sha=${tracking_ref}&per_page=20' --jq '.[] | .sha[0:8] + \" \" + (.commit.message | split(\"\\n\")[0])'" echo " gh api 'repos/${changes_slug}/commits/' --jq '.commit.message, (.files[] | .filename)'" echo " gh api 'repos/${changes_slug}/commits//pulls' --jq '.[] | .html_url + \" \" + .title'" fi diff --git a/global-settings/tests/test-check-settings-version.sh b/global-settings/tests/test-check-settings-version.sh index e5181bc38..8a680b62f 100644 --- a/global-settings/tests/test-check-settings-version.sh +++ b/global-settings/tests/test-check-settings-version.sh @@ -160,7 +160,8 @@ lacks 'OUT OF SYNC'; check "update: no OUT OF SYNC notice alongside UPDATE REQUI changelog_versions() { awk '/^## / { print $2 }' "$CANON_GS/CHANGELOG.md" | sort -t. -k1,1nr -k2,2nr -k3,3nr; } summary_of() { awk -v v="$1" '/^## / { if (f) exit; f = ($2 == v); next } f && NF { print; exit }' "$CANON_GS/CHANGELOG.md"; } CUR=$(cat "$SRC_DIR/VERSION") -[[ -n "$(summary_of "$CUR")" ]]; check "changelog: CHANGELOG.md has a one-sentence entry for VERSION $CUR" $? +has_entry() { [[ -n "$(summary_of "$1")" ]]; } +has_entry "$CUR"; check "changelog: CHANGELOG.md has a one-sentence entry for VERSION $CUR" $? eq "$(changelog_versions | head -1)" "$CUR"; check "changelog: the newest CHANGELOG.md entry is the current VERSION" $? mapfile -t CL_VERSIONS < <(changelog_versions) has 'What this update adds:'; check "changes: block present in the update notice" $? From c930f8aa80e0acda643101e909e25f810278536f Mon Sep 17 00:00:00 2001 From: WilcoLouwerse Date: Fri, 2 Oct 2026 15:29:51 +0200 Subject: [PATCH 4/6] docs(global-settings): date every changelog entry and list the never-released numbers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each heading now carries the date the version reached main (`## X.Y.Z — YYYY-MM-DD`); for 1.0.0 to 1.3.0 that is the main of claude-code-config. 1.2.1, 1.5.1 and 2.0.0 reached main together with the next version, and their entries say so. 1.5.0, 1.5.2, 1.5.3 and 2.4.3 get a `###` entry with a note on where their changes went. The update notice reads only `##` headings, so it does not list them. The README bump rule names the dated heading. Co-Authored-By: Claude Opus 5.5 --- global-settings/CHANGELOG.md | 90 ++++++++++++++++++++++-------------- global-settings/README.md | 2 +- 2 files changed, 57 insertions(+), 35 deletions(-) diff --git a/global-settings/CHANGELOG.md b/global-settings/CHANGELOG.md index fac54cfa1..a7aa18fcc 100644 --- a/global-settings/CHANGELOG.md +++ b/global-settings/CHANGELOG.md @@ -1,135 +1,157 @@ # Global settings changelog -One entry per released `VERSION`, newest first. The heading is the bare version number. The first line under it is **one sentence** that says what the update adds or fixes, in words a developer understands without opening the code. +One entry per released `VERSION`, newest first. The heading is the version number and the date it reached `main` (`## X.Y.Z — YYYY-MM-DD`). The first line under it is **one sentence** that says what the update adds or fixes, in words a developer understands without opening the code. `check-settings-version.sh` shows that sentence in the session-start UPDATE REQUIRED notice for every version between the installed one and the latest (since v2.8.0). The details live in the commit and the pull request. When a user asks for more, Claude looks those up from the version number. This file is not installed into `~/.claude/` and needs no `VERSION` bump of its own, but every `VERSION` bump needs an entry here (see [Bumping the version](README.md#️-bumping-the-version--required-on-every-change)). -Versions 1.0.0 to 1.3.0 were released from the now archived `ConductionNL/claude-code-config` repository. Numbers that only ever existed on a branch (1.5.0, 1.5.2, 1.5.3, 2.4.3) were never released and have no entry. +Versions 1.0.0 to 1.3.0 were released from the now archived `ConductionNL/claude-code-config` repository, and their date is the day they reached that repository's `main`. A few versions reached `main` together with the next one instead of on their own; their entry says so, and carries the date they reached `main`. Numbers that `main` never carried (1.5.0, 1.5.2, 1.5.3, 2.4.3) are listed under a `###` heading with a note on where their changes went. The update notice reads only `##` headings, so it skips them. -## 2.8.0 +## 2.8.0 — 2026-10-02 The update notice is now short at session start and says in one sentence what each pending version adds. Ask Claude how to update for the full steps, or for the details of the changes with links to the pull requests. -## 2.7.2 +## 2.7.2 — 2026-10-01 An unauthorized `git push` is now denied in every form, including `git -C push`, `git -c k=v push` and a push chained after a command Claude was asked to approve. -## 2.7.1 +## 2.7.1 — 2026-10-01 Picking a model or an effort level no longer reports `settings.json` as out of sync, because the keys Claude Code itself writes for that choice are ignored in the file check. -## 2.7.0 +## 2.7.0 — 2026-10-01 The update notice now installs every hook `settings.json` registers instead of a hand-kept list, and a session start checks that the installed files match the canonical copies. -## 2.6.0 +## 2.6.0 — 2026-09-29 Changes to production (`kubectl`, `oc` or `helm` writes against a `*-prod` namespace or context) are now hard-blocked, and text inside commit messages or heredocs no longer counts as a command. -## 2.5.2 +## 2.5.2 — 2026-09-28 The relock step now offers two options, a full lock or a lock without `settings.json`, so the VSCode model picker can keep working. -## 2.5.1 +## 2.5.1 — 2026-09-28 A push phrase typed next to a pasted image is recognised again, and a skill's text can no longer authorize a push by itself. -## 2.5.0 +## 2.5.0 — 2026-09-12 Hand-rolled waiting (CI watch commands, poll loops, long sleeps and idle heartbeats) is now blocked. -## 2.4.6 +## 2.4.6 — 2026-09-11 The unlock step now includes the `chmod u+w` that every update after the first needs, so an update no longer stalls halfway with `Permission denied`. -## 2.4.5 +## 2.4.5 — 2026-09-10 Seven `Write(...)` deny rules that Claude Code never applied are removed; the `Edit(...)` rules next to them already cover every file-editing tool. -## 2.4.4 +## 2.4.4 — 2026-09-10 The in-place edit guard no longer hard-denies a chain of harmless commands because the tool, its `-i` flag and the protected path came from different commands. -## 2.4.2 +### 2.4.3 — never released + +Two pull requests both took 2.4.3 on 2026-09-08, and `main` never carried it. Each branch was renumbered when `main` was merged into it, before its own merge on 2026-09-10. The destructive-guard fix ([#720](https://github.com/ConductionNL/.github/pull/720)) shipped as 2.4.4 and the removal of the inert `Write(...)` deny rules ([#716](https://github.com/ConductionNL/.github/pull/716)) as 2.4.5. + +## 2.4.2 — 2026-09-08 The update notice now also installs `sound-notify.sh` and `user-hooks-dispatch.sh`, which it had left out since they were added. -## 2.4.1 +## 2.4.1 — 2026-09-08 The `mcpServers` block is removed from `settings.json`, because Claude Code never read it there and the browsers it listed never loaded. -## 2.4.0 +## 2.4.0 — 2026-09-04 You can register your own hooks in `~/.claude/user-hooks.json`, and they survive every settings update. -## 2.3.1 +## 2.3.1 — 2026-09-04 The "up to date" message names its source ("via GitHub") again. -## 2.3.0 +## 2.3.0 — 2026-09-04 The canonical source moves back from Codeberg to GitHub (`ConductionNL/.github`), for the version check and for the update itself. -## 2.2.2 +## 2.2.2 — 2026-07-10 The permission-request sound now plays, because it is wired to the event Claude Code actually fires for a permission dialog. -## 2.2.1 +## 2.2.1 — 2026-07-09 Short notification sounds are no longer cut off when the hook exits. -## 2.2.0 +## 2.2.0 — 2026-07-09 Optional notification sounds when Claude asks a question, needs a permission or finishes, off by default and set in `~/.claude/sound-config.sh`. -## 2.1.0 +## 2.1.0 — 2026-06-12 `--legacy-peer-deps` is hard-denied for npm, pnpm, yarn and bun installs. -## 2.0.0 +## 2.0.0 — 2026-06-12 Breaking: the canonical source moves from GitHub to Codeberg (`codeberg.org/Conduction/.github`), so the version check and the update pull from there. -## 1.7.0 +It was committed on 2026-05-29 on the `feature/codeberg-global-settings-flip` branch and reached `main` in the same merge as 2.1.0, so `main` went from 1.7.0 straight to 2.1.0. + +## 1.7.0 — 2026-05-13 Claude's config files get a kernel-level lock (`chattr +i`), and a new hook stops the Write and Edit tools from changing them. -## 1.6.1 +## 1.6.1 — 2026-05-12 A push phrase now stays valid after Claude has run other tools in the same turn. -## 1.6.0 +## 1.6.0 — 2026-05-04 Tighter guards on writes to `~/.claude/`, `settings-repo-path` is no longer unlocked during an update, and the guard hook gets a test suite. -## 1.5.1 +### 1.5.3 — never released + +This number appears only in the commit message of the 1.6.0 change ("Bump VERSION 1.5.3 → 1.6.0"). That commit changed `VERSION` from 1.5.1 to 1.6.0, and no commit with `VERSION` 1.5.3 exists, so its changes are the ones listed under 1.6.0. + +### 1.5.2 — never released + +The push-phrase fix first took 1.5.2 on 2026-05-07, on a branch of a copy of `global-settings` kept in another repository. That branch was never merged. The same fix reached `main` here as 1.6.1 ([#43](https://github.com/ConductionNL/.github/pull/43)). + +## 1.5.1 — 2026-05-04 You can track a branch other than `main` with `~/.claude/settings-repo-ref`, and destructive commands such as `sudo`, `rm -rf /`, `git reset --hard` and `gh pr merge` are denied. -## 1.4.0 +It reached `main` together with 1.6.0, in [#22](https://github.com/ConductionNL/.github/pull/22), so `main` went from 1.4.0 straight to 1.6.0. + +### 1.5.0 — never released + +On 2026-04-13 a copy of `global-settings` kept in another repository took 1.5.0 for the destructive-command deny rules and went to 1.5.1 the same day. This repository went from 1.4.0 straight to 1.5.1, so those deny rules shipped as part of 1.5.1. + +## 1.4.0 — 2026-04-16 The version check reads the latest version straight from GitHub (`~/.claude/settings-repo-url`), with the local clone as fallback. -## 1.3.0 +## 1.3.0 — 2026-04-08 Write requests through `curl` are caught in more forms, including combined flags such as `-sX POST` and a `curl` behind a pipe. -## 1.2.2 +## 1.2.2 — 2026-04-03 `gh pr`, `gh issue`, `gh repo`, `gh release`, `gh workflow` and `gh run` write commands now ask for approval, also when chained after a read. -## 1.2.1 +## 1.2.1 — 2026-04-03 More read-only commands run without a prompt, and any output redirect to a file now asks for approval. -## 1.2.0 +It reached `main` of `claude-code-config` together with 1.2.2 (PR #20 there), so that `main` went from 1.2.0 straight to 1.2.2. + +## 1.2.0 — 2026-03-28 Claude can no longer change its own settings files or make them writable, and the version check retries once when the remote is briefly unreachable. -## 1.1.0 +## 1.1.0 — 2026-03-28 Commands that would leave the WSL workspace (Windows drives under `/mnt//`, `.exe` programs, `wsl`) are hard-blocked. -## 1.0.0 +## 1.0.0 — 2026-03-27 The first mandatory, versioned global settings: a shared `settings.json`, the write-guard hook and a session-start version check. diff --git a/global-settings/README.md b/global-settings/README.md index 5be38f174..86cc93a3f 100644 --- a/global-settings/README.md +++ b/global-settings/README.md @@ -322,7 +322,7 @@ Before v2.7.2 the push check ran after the `git -C`, `gh`, `curl` and `docker` p Failing to bump the version means users will not be warned to update, and their installed settings will silently fall behind. -**Every bump also adds an entry to [`CHANGELOG.md`](CHANGELOG.md)**: a `## X.Y.Z` heading with one sentence under it that says what the update adds, in words a developer understands without opening the code. The update notice shows that sentence. A missing entry fails the version-check tests. +**Every bump also adds an entry to [`CHANGELOG.md`](CHANGELOG.md)**: a `## X.Y.Z — YYYY-MM-DD` heading, dated the day the version reaches `main`, with one sentence under it that says what the update adds, in words a developer understands without opening the code. The update notice shows that sentence. A missing entry fails the version-check tests. Changes to `global-settings/README.md` alone do **not** require a bump: nothing installed changes, so a bump would push every developer through the unlock/relock cycle to copy a file they don't have. Documentation under `docs/claude/` is likewise never a trigger. From b5924df59e9acada87b03b334d9545c7986ca552 Mon Sep 17 00:00:00 2001 From: WilcoLouwerse Date: Fri, 2 Oct 2026 16:22:23 +0200 Subject: [PATCH 5/6] test(global-settings): fail a changelog heading without a date MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every `## X.Y.Z` heading in CHANGELOG.md must carry a date (`— YYYY-MM-DD`, month 01-12, day 01-31), and the dates may not rise going down the file. A fixed bad file proves the check catches a missing date, an impossible month and a date out of order. The README bump rule says so. Co-Authored-By: Claude Opus 5.5 --- global-settings/README.md | 2 +- .../tests/test-check-settings-version.sh | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/global-settings/README.md b/global-settings/README.md index 86cc93a3f..58e8e8278 100644 --- a/global-settings/README.md +++ b/global-settings/README.md @@ -322,7 +322,7 @@ Before v2.7.2 the push check ran after the `git -C`, `gh`, `curl` and `docker` p Failing to bump the version means users will not be warned to update, and their installed settings will silently fall behind. -**Every bump also adds an entry to [`CHANGELOG.md`](CHANGELOG.md)**: a `## X.Y.Z — YYYY-MM-DD` heading, dated the day the version reaches `main`, with one sentence under it that says what the update adds, in words a developer understands without opening the code. The update notice shows that sentence. A missing entry fails the version-check tests. +**Every bump also adds an entry to [`CHANGELOG.md`](CHANGELOG.md)**: a `## X.Y.Z — YYYY-MM-DD` heading, dated the day the version reaches `main`, with one sentence under it that says what the update adds, in words a developer understands without opening the code. The update notice shows that sentence. A missing entry, or a heading without a date, fails the version-check tests. Changes to `global-settings/README.md` alone do **not** require a bump: nothing installed changes, so a bump would push every developer through the unlock/relock cycle to copy a file they don't have. Documentation under `docs/claude/` is likewise never a trigger. diff --git a/global-settings/tests/test-check-settings-version.sh b/global-settings/tests/test-check-settings-version.sh index 8a680b62f..e47f5daaa 100644 --- a/global-settings/tests/test-check-settings-version.sh +++ b/global-settings/tests/test-check-settings-version.sh @@ -14,7 +14,8 @@ # - the update notice shows the CHANGELOG.md sentence of every pending # version, newest first and capped at five, says when one is missing or # the file cannot be fetched, and gives Claude the commit/PR lookup; and -# CHANGELOG.md has an entry for the current VERSION +# CHANGELOG.md has an entry for the current VERSION, and every entry +# heading carries a date, newest first # - a matching install is reported up to date, with every file verified # - a missing or changed hook, or a changed settings.json, is reported # FILES OUT OF SYNC with repair blocks for those files only @@ -163,6 +164,22 @@ CUR=$(cat "$SRC_DIR/VERSION") has_entry() { [[ -n "$(summary_of "$1")" ]]; } has_entry "$CUR"; check "changelog: CHANGELOG.md has a one-sentence entry for VERSION $CUR" $? eq "$(changelog_versions | head -1)" "$CUR"; check "changelog: the newest CHANGELOG.md entry is the current VERSION" $? +# heading_problems — every "## " heading that is not "## X.Y.Z — YYYY-MM-DD" +# with a month 01-12 and a day 01-31, or whose date is later than the one above +# it. The "### " headings of never-released numbers are not checked. +heading_problems() { + awk '/^## / { + if ($0 !~ /^## [0-9]+\.[0-9]+\.[0-9]+ — [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/) { print "format: " $0; next } + d = $4; m = substr(d, 6, 2) + 0; dd = substr(d, 9, 2) + 0 + if (m < 1 || m > 12 || dd < 1 || dd > 31) { print "date: " $0; next } + if (prev != "" && d > prev) print "order: " $0 + prev = d + }' "$1" +} +eq "$(heading_problems "$CANON_GS/CHANGELOG.md")" ""; check "changelog: every entry heading is dated, newest first" $? +printf '## 9.9.9\n\nx\n\n## 9.9.8 — 2026-13-01\n\nx\n\n## 9.9.7 — 2020-01-01\n\nx\n\n## 9.9.6 — 2021-01-01\n\nx\n' > "$TMP/CHANGELOG.bad.md" +eq "$(heading_problems "$TMP/CHANGELOG.bad.md" | cut -d: -f1 | tr '\n' ' ')" "format date order " +check "changelog: the date check catches a missing date, an impossible month and a date out of order" $? mapfile -t CL_VERSIONS < <(changelog_versions) has 'What this update adds:'; check "changes: block present in the update notice" $? has "v${CUR} — $(summary_of "$CUR")"; check "changes: the latest version's sentence is shown" $? From 17ecdf72c72e33369b3fe1f8c05c3519b6b02846 Mon Sep 17 00:00:00 2001 From: WilcoLouwerse Date: Fri, 2 Oct 2026 16:50:27 +0200 Subject: [PATCH 6/6] docs(global-settings): add the 2.7.3 changelog entry Co-Authored-By: Claude Opus 5.5 --- global-settings/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/global-settings/CHANGELOG.md b/global-settings/CHANGELOG.md index a7aa18fcc..97181368b 100644 --- a/global-settings/CHANGELOG.md +++ b/global-settings/CHANGELOG.md @@ -10,6 +10,10 @@ Versions 1.0.0 to 1.3.0 were released from the now archived `ConductionNL/claude The update notice is now short at session start and says in one sentence what each pending version adds. Ask Claude how to update for the full steps, or for the details of the changes with links to the pull requests. +## 2.7.3 — 2026-10-02 + +A hard block anywhere in a chained command now wins over a prompt for another part of it, and a git alias that pushes (or a command that defines one) needs the push phrase just like `git push`. + ## 2.7.2 — 2026-10-01 An unauthorized `git push` is now denied in every form, including `git -C push`, `git -c k=v push` and a push chained after a command Claude was asked to approve.