Use git update-index Safely for Precise Index Changes

git update-index is the plumbing behind git add, for when you need exact control over Git's staging index instead of the friendly defaults. For ordinary staging, git add is easier to reason about; this is for scripts and edge cases. Allow about 15 minutes, including the checks. The examples assume an existing repository and need no elevated privileges.

1. Check the installed Git and repository state

The installed command here is Git 2.43.0. Options and diagnostic wording can differ between releases, so check your own version before comparing a script or a captured transcript against this guide.

$ git --version
git version 2.43.0
$ git rev-parse --show-toplevel
/path/to/repository
$ git status --short

The last command may print nothing, or it may show existing changes. Do not run these examples against a repository holding work you cannot afford to stage: git update-index changes the index, not the commit, and that change is visible to later git diff --cached and commit operations.

Checkpoint: confirm the repository path, and save the output of git status --short if you need to tell your test apart from existing work.

2. Register an edited tracked file

With a tracked file changed in the working tree, pass its path without extra flags. Git reads the file and records its current contents in the index; it does not create a commit.

$ git update-index -- path/to/tracked-file.txt
$ git diff --cached --name-status
M	path/to/tracked-file.txt

The -- ends options, which matters when a path begins with a hyphen. The staged diff now describes the index against HEAD; git diff -- path/to/tracked-file.txt describes what is still unstaged. No remaining edits, and that second command prints nothing.

New files are ignored by default. Add one explicitly with --add:

$ git update-index --add -- path/to/new-file.txt
$ git status --short
A  path/to/new-file.txt

Staged the wrong new file? git restore --staged -- path/to/new-file.txt removes it from the index while leaving the working file in place. Ordinary, unprivileged Git: check the result with git status --short.

3. Remove a missing path deliberately

If a tracked file has been deleted from the working tree, the default command ignores that absence. Use --remove when the deletion is intentional:

$ git update-index --remove -- path/to/old-file.txt
$ git diff --cached --name-status
D	path/to/old-file.txt

This stages the deletion in the index. It does not remove another copy, restore the file, or touch HEAD. Deleted by mistake? Before committing, recover it with git restore --staged --worktree -- path/to/old-file.txt. That restores the last committed version, so stop first if you actually need the uncommitted contents of the deleted file.

4. Refresh metadata without replacing file contents

--refresh checks filesystem stat data against the index. Useful after something like git read-tree, but it does not compute a new object ID and it will not make real content or mode changes disappear. Treat it as inspection and maintenance, never as a substitute for staging an edit.

$ git update-index --refresh
$ git status --short

5. Treat assume-unchanged and skip-worktree as sharp tools

--assume-unchanged tells Git to skip its normal checks for a path and trust your promise that it has not changed. It exists for slow filesystem stat calls, not for permanently hiding local edits. Check the marker with git ls-files -v: a lower-case h means assume-unchanged on the installed Git.

$ git update-index --assume-unchanged -- path/to/local-file
$ git ls-files -v -- path/to/local-file
h path/to/local-file
$ git update-index --no-assume-unchanged -- path/to/local-file
$ git ls-files -v -- path/to/local-file
H path/to/local-file

Edit the file, and clear the bit before expecting ordinary change detection to work again. If an upstream merge needs to touch an assumed path, Git can stop and demand manual handling; the fix is the same --no-assume-unchanged command, then check git diff and git status.

--skip-worktree does something different: it tells Git to avoid writing a path to the working directory where it reasonably can, and to tolerate its absence. It is mainly an implementation detail of sparse checkout. Prefer git sparse-checkout for sparse working trees, and never treat either bit as a reliable way to ignore changes to tracked configuration files.

$ git update-index --skip-worktree -- path/to/sparse-file
$ git ls-files -v -- path/to/sparse-file
S path/to/sparse-file
$ git update-index --no-skip-worktree -- path/to/sparse-file

Clear the bit before normal editing or recovery. Git commands can still write a skip-worktree file during important operations such as a conflict, so do not treat this as a security boundary.

6. Test optional index acceleration before enabling it

The untracked cache can speed up commands like git status by recording directory metadata. Test filesystem support first:

$ git update-index --test-untracked-cache
Testing mtime in '/path/to/repository' ...... OK

Status 0 and OK printed means the test succeeded. Only then consider git update-index --untracked-cache. To undo the extension, use git update-index --no-untracked-cache. Git's manual recommends the core.untrackedCache setting for a lasting policy, but change repository configuration only after you have checked the effect on your own workflow.

Split index mode is another performance option for large indexes: it stores a shared index file under $GIT_DIR. Enable it only when you have a reason to measure the change, and undo it with git update-index --no-split-index. Never delete shared index files by hand.

7. Verify the final index before committing

Review both sides of the worktree and index boundary:

$ git diff --name-status
$ git diff --cached --name-status
$ git status --short
$ git diff --cached --check

The first shows unstaged work, the second shows staged work, the third combines both views. git diff --cached --check reports whitespace errors in the staged patch and prints nothing when it finds none. Index wrong? Use git restore --staged for individual paths, or git reset HEAD -- path/to/file on older scripts that still need that spelling. Neither command touches committed history.

Done means