skip to content

What is the difference between git update-index --assume-unchanged and --skip-worktree?

level: seniorimportance: nice to knowfreq 30%

answer

  1. both are local index bits, never pushed
  2. one is a promise, one is a declaration
  3. only one of them survives a pull reliably
  4. one underpins sparse checkout

basics

~20 s

Both set a bit on an index entry that hides working-tree edits to a tracked file. --assume-unchanged is a performance promise Git may discard and overwrite; --skip-worktree declares the index version authoritative and is respected and preserved far more carefully.

solid answer

~60 s

Both flags mark an entry in `.git/index` so that Git stops comparing that path against the working tree, but they mean different things. `--assume-unchanged` is a promise *you* make to Git: "this file will not change, skip the stat check." It exists as a performance escape hatch for slow filesystems. Git treats it as advisory — operations that rewrite the index, such as checkout, merge or pull, may clear the bit and overwrite your file. `--skip-worktree` says the opposite: the working-tree copy is not authoritative, so keep the index version and avoid touching the file on disk. Git preserves the bit across operations and tries hard not to clobber the file; it is the same mechanism sparse-checkout uses to mark paths that are absent from the working tree. Neither is a way to keep local edits to a tracked config file out of commits. The bit lives in one index, is never pushed, silently drops your changes, and disappears on a fresh clone. Use an ignored file plus a committed template instead.

code

console · 6 lines
console
$ git update-index --skip-worktree config/local.yml
$ git ls-files -v config/local.yml
S config/local.yml
$ git update-index --no-skip-worktree config/local.yml
$ git ls-files -v config/local.yml
H config/local.yml

go deeper

for a junior

Know that both are advanced plumbing flags set with git update-index, that they hide changes to a tracked file locally, and that neither is the normal way to ignore something.

for a middle

Explain the split: assume-unchanged is a performance promise Git may discard, skip-worktree declares the index copy authoritative and is preserved, and both live only in your local index.

for a senior

Diagnose the failure they cause — silently lost local edits after a pull, invisible divergence from the committed config — and redirect to untracking plus a committed template.

for a principal

Treat per-developer index hacks as a symptom of a configuration design problem, and set the standard that environment-specific values live in ignored files or environment variables rather than hidden local state.

## The shared mechanism Every entry in `.git/index` carries flag bits alongside its path, mode, blob id and cached stat data. `git update-index` is the plumbing command that edits those entries directly, and two of its flags set bits that make Git stop reporting working-tree changes for a path. Both are strictly local: the index is not a tracked object, so nothing about either bit is pushed, fetched, cloned or seen by anyone else. Inspect the current state with `git ls-files -v`. The tag letter in the first column tells you which bit is set: `S` marks a skip-worktree entry, and a lowercase tag letter means assume-unchanged. Clear them with `--no-assume-unchanged` and `--no-skip-worktree`. ## --assume-unchanged: a performance hint The documented purpose is filesystems where `lstat` is expensive. You promise not to modify the path, and in exchange Git is allowed to skip checking it, which can shave real time off `git status` in a huge tree. Because it is a promise rather than an instruction, Git reserves the right to ignore it. Any operation that needs to update the index for that path — a branch switch, a merge, a pull that touches the file — may clear the bit and replace the file with the committed content. There is no warning and no reflog entry for the working-tree copy, because your edits were never in the object database. That is the failure mode people hit: months of quiet success, then a routine pull silently overwrites their local settings. ## --skip-worktree: the index copy wins Here the statement is about authority rather than performance. The index entry is declared the truth for that path, so Git should not read the file and should avoid overwriting it. Git preserves the bit across index-updating operations and refuses some operations rather than clobbering a skipped file, which makes it noticeably stickier in practice than assume-unchanged. Its real design purpose is sparse checkout: paths outside your sparse pattern set stay in the index, marked skip-worktree, with no corresponding file on disk. That is why the flag is well maintained — it is load-bearing for a shipped feature rather than a corner in the plumbing. ## Why neither solves the local-config problem The question is nearly always asked because someone wants to edit a tracked configuration file without ever committing the change. Both bits appear to do it, and both are the wrong answer: - **Not shareable.** Each teammate must run the command by hand on every clone, and nothing reminds them. - **Lost silently.** A fresh clone, a new worktree, or anything that rebuilds the index starts without the bit. - **Dangerous in the other direction.** When upstream legitimately changes that file, you either miss the change or hit an obscure failure, and with assume-unchanged you can simply lose your edits. - **Actively deceptive.** Your local edits are invisible to `git status`, so a real change to that file will never be committed, and the divergence between what you run and what the repository says compounds over time. The answer an interviewer is listening for is structural: do not track the environment-specific file at all. Commit a `config.example` template, ignore the real path, and have the application or a setup script produce it. If a tracked file must vary per environment, that variation belongs in an environment variable or a layered configuration mechanism, not in a hidden index bit. The legitimate uses are narrow — assume-unchanged as a measured performance workaround on a pathologically slow filesystem, and skip-worktree as an implementation detail of sparse checkout — and both are best kept out of everyday developer workflow advice.

  • Which command shows you which tracked files currently carry one of these bits?
    `git ls-files -v` prints a tag letter before each path. `S` means skip-worktree, and a lowercase tag letter means assume-unchanged; `H` is an ordinary cached entry. Clear the bits with `git update-index --no-skip-worktree` or `--no-assume-unchanged` on the path.
  • A developer used one of these bits to keep local database credentials in a tracked config file. What do you recommend instead?
    Stop tracking the file: add an ignore rule, untrack it, and commit a checked-in template plus a setup step that generates the real one. If the file must stay tracked, move the varying values into environment variables or a layered config file that the tracked file reads. A hidden per-clone index bit is invisible to reviewers and disappears on the next clone.
  • Why is assume-unchanged the more dangerous of the two?
    It is only a hint. Git may clear the bit and overwrite the file whenever an operation updates that index entry — a checkout, merge or pull — and because your edits were never hashed into an object, there is nothing to recover them from. Skip-worktree declares the index authoritative and Git works to preserve both the bit and the file.

saying these in an interview costs you the question

  • Recommends either flag as the way to ignore a tracked file
  • Believes the bit is shared with teammates through a push
  • Thinks assume-unchanged reliably protects local edits
  • Cannot say which one sparse checkout relies on
  • Assumes a fresh clone keeps the marking

context