Skip to content

Stop Claude Code's settings rewrites from dirtying the tree - #6

Merged
jpmontez merged 1 commit into
mainfrom
claude-settings-clean-filter
Sep 9, 2026
Merged

jpmontez merged 1 commit into
mainfrom
claude-settings-clean-filter

Conversation

@jpmontez

@jpmontez jpmontez commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Problem

~/.claude/settings.json is stowed from this repo, but Claude Code owns that file — it rewrites the whole thing whenever a runtime option changes. Every /model switch left the tree dirty with a key reorder plus a one-line model change.

There's no env var or setting to redirect where Claude Code persists this; the only non-written settings source is the --settings flag, which is per-invocation and wouldn't cover desktop-app or IDE launches (so the permissions.deny rules would silently not apply there).

Fix, in two halves

Neither half is sufficient alone.

1. Clean filter — keeps commits clean. .gitattributes routes the file through claude-settings-clean.py, which drops the keys Claude Code writes back as machine state (model, theme, sandbox, modelSettings) and sorts the rest, so a rewrite in a different key order isn't a diff either. Invalid JSON round-trips untouched rather than wedging git.

2. skip-worktree — keeps git status clean. A clean filter alone does not quiet git status: git's index-refresh path hashes the worktree file raw, and only the diff machinery applies the filter. So the file reads as modified even when it cleans to exactly the committed blob:

$ git status --porcelain     #  M claude/.claude/settings.json
$ git diff --numstat         #  (empty)
$ git hash-object --path claude/.claude/settings.json claude/.claude/settings.json
730cd9e...                   #  identical to the index blob

Reproduced in an isolated repo on git 2.55.0; filter.<name>.required does not change it.

Wiring

Both are per-clone git state that .gitattributes can't carry, so bootstrap.sh sets them and doctor.sh reports a clone missing either — the same install/detect split as the Brewfiles.

== Repo
  ✓ Claude settings clean filter registered
  ✓ Claude settings marked skip-worktree
  ✓ working tree clean

Cost

Committing a deliberate change to that settings file needs a toggle (--no-skip-worktree, commit, --skip-worktree). Documented in CLAUDE.md, since while it's hidden git ignores edits you meant to keep.

Verification

Simulated a /model switch (model + theme changed, top-level keys reordered) against a clean tree: git status stayed empty, and the live file kept every setting. shellcheck bootstrap.sh doctor.sh lib.sh macos/*.sh clean. ./doctor.sh green.

Also ignores **/.claude/.cc-writes/, which Claude Code creates inside the repo.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AEPYeZgMsmFPVBbRvkFtM2

~/.claude/settings.json is stowed from this repo, but Claude Code owns it: it
rewrites the whole file whenever a runtime option changes, so every /model
switch left the working tree dirty with a key reorder and a one-line model
change. There's no env var or setting to redirect where it persists — only the
--settings flag, which is per-invocation and wouldn't cover GUI or IDE launches.

Handle it in two halves, because neither is sufficient alone:

.gitattributes routes the file through claude-settings-clean.py, which drops
the keys Claude Code writes back as machine state (model, theme, sandbox,
modelSettings) and sorts the rest so a rewrite in a different key order isn't a
diff either. That keeps commits clean but not `git status` — git's index
refresh hashes the worktree file raw and only the diff machinery applies the
filter, so the file reads as modified even when it cleans to the committed
blob. Verified against git 2.55.0; filter.<name>.required doesn't change it.

skip-worktree is what actually silences the churn. It costs a toggle to commit
a deliberate change to the file, which CLAUDE.md documents.

Both are per-clone git state that .gitattributes can't carry, so bootstrap.sh
sets them and doctor.sh reports a clone that's missing either — the same
pattern as the Brewfiles.

Also ignore **/.claude/.cc-writes/, which Claude Code creates in the repo.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AEPYeZgMsmFPVBbRvkFtM2
@jpmontez
jpmontez merged commit a268ac8 into main Sep 9, 2026
2 checks passed
@jpmontez
jpmontez deleted the claude-settings-clean-filter branch September 9, 2026 00:02
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