Back to Blog

The 'Assume-Unchanged' Trap: Why Your Local Config Changes Keep Leaking Into PRs

The 'Assume-Unchanged' Trap: Why Your Local Config Changes Keep Leaking Into PRs

There’s a specific kind of internal sigh that happens when you’re reviewing your own PR and realize you’ve just committed your local database credentials for the third time this month. It’s usually a small thing—a dev-mode flag or a local port change—that was never meant to leave your machine, yet there it is, staring back at you from the GitHub diff.

Most of us, in a fit of frustration, eventually find a StackOverflow answer suggesting a "quick fix" to stop Git from tracking changes to a file that's already in the repository:

git update-index --assume-unchanged config/settings.json

It feels like magic. The file disappears from your git status, you can go about your business, and your PRs stay clean. But here’s the thing: assume-unchanged is a trap. It’s a performance optimization tool masquerading as a configuration management strategy, and it will eventually break your workflow in ways that are incredibly annoying to debug.

The Performance Lie

The primary reason assume-unchanged exists isn't to help you manage local settings. It was designed for slow file systems where Git's lstat(2) calls are expensive. By marking a file as assume-unchanged, you’re telling Git: "Don't even bother checking this file for changes because I promise it hasn't moved."

When you use it to hide config changes, you’re lying to Git. And Git, being a literal-minded tool, believes you.

Why it inevitably bites you

The trouble starts the moment someone else on your team actually *does* need to change that file.

If a teammate updates config/settings.json and you pull their changes, Git hits a wall. Because you told Git the file would never change, it doesn't know how to handle the incoming merge. You’ll get an error message that is about as clear as mud, usually something like Your local changes to the following files would be overwritten by merge, even though git status insists everything is clean.

You’re left in a ghost-hunting scenario:
1. You can’t pull because of "local changes."
2. You can’t see the local changes because they're ignored.
3. You can't stash the changes because Git thinks there are no changes to stash.

A better (but still temporary) path: --skip-worktree

If you absolutely must keep local changes to a tracked file and you can't change the architecture of the project right now, use this instead:

git update-index --skip-worktree config/settings.json

While they look similar, the intent is different. skip-worktree tells Git: "I have modified this file locally, and I want to keep my version, regardless of what's in the index."

If the file changes upstream, Git will actually try to handle it more gracefully. However, it still isn't a silver bullet. If there’s a conflict between the upstream change and your local hidden change, the pull will still fail. The difference is that skip-worktree is more honest about why it’s failing.

Finding the "Invisible" Files

If you’ve already gone down the rabbit hole and can't remember which files you've hidden, you can use this command to find them. It lists all files and their status; lowercase letters usually indicate the files you've messed with.

git ls-files -v | grep '^[a-z]'

To "un-hide" them and get back to a sane state:

git update-index --no-assume-unchanged <file>
# OR
git update-index --no-skip-worktree <file>

The Real Fix: Architectural Hygiene

The fact that you’re reaching for update-index is usually a symptom of a design smell. Tracked files should be generic; local variations should live in untracked files.

The industry-standard approach for a reason is the .example pattern.
1. Commit a config/settings.json.example file with dummy values.
2. Add the actual config/settings.json to your .gitignore.
3. Each developer copies the example file to the real filename and adds their local secrets.

If you’re working on a legacy project where the config file is already tracked and you can't delete it without breaking the build, consider using a local override file. Many modern frameworks look for a .local suffix (e.g., settings.json and settings.local.json). You track the first, and ignore the second.

It takes five minutes of extra effort to set up a proper ignore-pattern, but it saves you the hour you’d eventually spend wondering why your git pull is failing on a "clean" working directory. Stop lying to Git; it has a long memory and a very low tolerance for being misled.