Skip to content

Stop chezmoi clobbering app-written settings keys - #17

Merged
DROOdotFOO merged 2 commits into
mainfrom
fix-settings-clobber
Aug 8, 2026
Merged

Stop chezmoi clobbering app-written settings keys#17
DROOdotFOO merged 2 commits into
mainfrom
fix-settings-clobber

Conversation

@DROOdotFOO

Copy link
Copy Markdown
Owner

~/.claude/settings.json has two writers. chezmoi treated it as a plain managed
file, so it was authoritative and every chezmoi apply deleted whatever Claude
Code had written at runtime:

$ chezmoi diff ~/.claude/settings.json
-  "model": "opus",
-  "effortLevel": "xhigh",

That is the recurring MM in chezmoi status -- the file diverges in both
directions, so apply clobbers the app and re-add clobbers the repo.

Fix

Convert the source to a modify_ script. chezmoi pipes the current target
to it on stdin and takes stdout as the new target, so it can merge instead of
overwrite:

printf '%s' "$current" | jq --argjson managed "$managed" '. * $managed'

jq's * merges objects recursively with the right-hand side winning, so
managed keys stay authoritative while anything the app added survives -- today
model and effortLevel, tomorrow whatever /config gains.

Two properties worth calling out:

  • Idempotent. chezmoi runs the script for status and diff, not just
    apply. If f(current) != current once converged, the file would report
    drift forever. Verified byte-identical output against the live file.
  • Degrades safely. Empty stdin (fresh machine), invalid JSON, or missing
    jq all fall back to emitting the managed block. Emitting nothing would
    truncate settings.json.

The trade-off: a merge only ever adds, so deleting a key from the source no
longer deletes it from the target. Documented.

Also

**/.claude/settings.local.json had been added straight to ~/.gitignore_global,
so apply kept trying to delete it. Folded into the chezmoi source, with a comment
explaining what that file is.

CLAUDE.md gains the general pattern, the two constraints above, and the two
files still unconverted that are known to rewrite themselves: ~/.config/zed/settings.json
(Zed writes UI changes; strict JSON, so the same recipe applies) and ~/.gitconfig
(gh auth setup-git rewrites its credential blocks with tabs and a trailing
space -- INI, so jq does not apply).

Manual testing

Six cases against the rendered script:

  • existing file on stdin -> model and effortLevel preserved
  • output byte-identical to the live file (idempotent, no perpetual drift)
  • empty stdin -> full managed config, 15 deny rules, hook paths expanded
  • invalid JSON on stdin -> recovers to the managed block
  • unrelated user key survives the merge
  • managed key wins over a stale target value
  • real chezmoi apply: runtime keys survived, managed keys correct, MM gone
  • chezmoi status clean afterwards; only R script entries remain
  • rendered script passes shellcheck; pre-commit passes

🤖 Generated with Claude Code

~/.claude/settings.json is owned by two writers. chezmoi treated it as
a plain managed file, so every apply deleted the keys Claude Code
writes at runtime -- model and effortLevel -- which is why it sat at
MM in chezmoi status and kept coming back after each apply.

Convert it to a modify_ source. chezmoi pipes the current target in on
stdin and takes stdout as the new target, so the managed block is
merged over the existing file with jq '. * $managed': managed keys win,
app-written keys survive. Falls back to the managed block when stdin is
empty, not valid JSON, or jq is missing, so a fresh machine is fine.

The merge is idempotent, which matters because chezmoi runs the script
for status and diff too -- a non-converging script would report drift
forever.

Also fold the settings.local.json rule into dot_gitignore_global. It
had been added to the target directly, so apply kept trying to delete
it, and document the pattern plus the two files still unconverted:
zed's settings.json and .gitconfig.
Zed rewrites ~/.config/zed/settings.json whenever a setting changes in
the UI -- theme, font size, panel docks -- so it would hit the same
clobber as ~/.claude/settings.json the first time the app wrote to it.
Converting now rather than after it bites.

settings.json is strict JSON, so the jq merge carries over unchanged.
First apply reformats arrays to jq's style; verified the script is
idempotent from there (pass 1 == pass 2), so this converges instead of
reporting drift forever.

Note the attribute order: modify_ precedes private_ in the source
filename, and the target keeps its 0600 mode.
@DROOdotFOO
DROOdotFOO merged commit 554a877 into main Aug 8, 2026
5 checks passed
@DROOdotFOO
DROOdotFOO deleted the fix-settings-clobber branch August 8, 2026 19:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant