Inspect Git's Index Without Guessing What It Stores
The index is the file behind every git status and git add, and this guide shows you how to read it without breaking anything. You will find its version and entry count, and match the binary file against paths shown by ordinary Git commands. The whole workflow is read-only: it does not stage, unstage, commit or rewrite anything. Allow 10 to 15 minutes for a repository with a small or medium index.
The route
Jump straight to the step you need, or tick off Done means at the end.
This guide describes the installed Git 2.43.0 and the gitformat-index(5) format documented with it. The index is an implementation file, not a hand-editing target. Use Git commands to change it, and reach for the byte-level checks below only when you need to understand or diagnose it.
1. Pick a repository and confirm the tools
Swap /path/to/repository for a working tree you can actually inspect. Skip a repository with an unrecorded operation if you are not sure what its state means. None of the commands below need elevated privileges as long as you can read the repository normally.
$ cd /path/to/repository
$ git --version
git version 2.43.0
$ git rev-parse --show-toplevel
/path/to/repository
Checkpoint: write the version down. Index details can shift between Git releases even when the everyday commands stay perfectly compatible.
2. Locate the active index
Git normally keeps the index at $GIT_DIR/index. Ask Git for the path rather than assuming .git is a plain directory, worktrees and other layouts can point somewhere else:
$ index_path=$(git rev-parse --git-path index)
$ printf '%s\n' "$index_path"
/path/to/repository/.git/index
$ test -r "$index_path" && echo 'index is readable'
index is readable
$ stat -c 'bytes: %s' "$index_path"
bytes: 1234
Your path and size will differ. A missing index is not automatically a problem: a freshly created repository can have zero entries and no index file at all until the first staging operation.
3. Read the header, without parsing it by hand
The file opens with a 12-byte header: a four-byte ASCII signature DIRC, a four-byte version, and a four-byte entry count. Binary integers use network byte order, so a hex dump is safer than trying to read the fields as decimal text.
$ od -An -tx1 -N12 "$index_path"
44 49 52 43 00 00 00 02 00 00 00 01
Here 44 49 52 43 spells DIRC, 00 00 00 02 is version 2, and the final four bytes say there is one entry. Git 2.43.0 supports index versions 2, 3 and 4. Most people should leave the version choice to Git rather than forcing a format for a tool that has not actually earned it.
For a machine-readable sanity check, compare that header reading against the entries Git itself reports:
$ git ls-files --stage
100644 ce013625030ba8dba906f756967f9e9ca394464a 0\treadme.txt
The object ID and mode here describe the staged entry, and the trailing 0 is its merge stage. This command beats opening the binary file in an editor as your first diagnostic, every time.
4. Inspect entry metadata and stages
Use --debug when you need the cached stat information Git keeps beside each path: ctime, mtime, device, inode, uid, gid, size and flags. These values explain why Git decides to refresh an entry, they are not a second copy of the working file.
$ git ls-files --debug
readme.txt
ctime: 1790227816:665821456
mtime: 1790227816:665821456
dev: 2306 ino: 125592963
uid: 1004 gid: 1004
size: 6 flags: 0
Exact numbers are host-specific. Each index entry carries cached filesystem data, the object name, a mode and flags, and its path is relative to the repository root. Entries are sorted by unsigned-byte path order, which is exactly why you should not sort names with unusual bytes using locale-sensitive rules.
During a merge conflict, one path can have several index entries at once. The stage field tells the base apart from the two sides, so check it before attempting any repair:
$ git ls-files --unmerged
100644 1111111111111111111111111111111111111111 1 conflicted.txt
100644 2222222222222222222222222222222222222222 2 conflicted.txt
100644 3333333333333333333333333333333333333333 3 conflicted.txt
The object IDs above are illustrative only, use the values your own repository prints, do not copy these into a command. Stage 1 is the common ancestor, stages 2 and 3 are the conflicting sides. Once you resolve the file, git add conflicted.txt replaces the higher-stage entries with a single stage-0 entry. That changes the index, so check git status first and keep a recovery plan for the working file.
5. Recognise extensions before touching unusual entries
After the entries, an index can carry extensions. Each one has a four-byte signature, a 32-bit size and its own data. A signature that starts with an uppercase ASCII letter marks an optional extension an implementation is free to ignore. A final checksum covers everything before it, using SHA-1 or SHA-256 to match the repository's object format.
Common extensions explain behaviour that would otherwise look odd:
TREEcaches directory tree information so Git can build or compare trees faster.REUCpreserves what is needed to recreate some resolved conflicts with commands such asgit checkout -m.linkrecords a split index, where most entries live in a shared index file under$GIT_DIR/sharedindex.<hash>.UNTRcaches untracked-file information, andFSMNrecords file-system-monitor state.sdirmarks sparse-directory entries that need sparse-index-aware tooling.
Warning: do not delete an extension or its shared index file as a cleanup experiment. That is destructive and can make Git rebuild or reject the index outright. If an index looks corrupt, back up the repository first, then use normal Git recovery procedures and a copy of the working tree.
6. Check sparse and intent-to-add flags through Git
Version 3 and later can carry an extended flags field. Sparse checkout uses the skip-worktree bit, and git add -N uses the intent-to-add bit. Treat these as state, not bytes to edit by hand. Inspect them through Git instead:
$ git ls-files --stage
$ git ls-files -v
H readme.txt
The exact letter from git ls-files -v depends on each entry's flags and repository state, so treat it as something to investigate rather than a universal expected line. If sparse checkout is active, confirm its configuration before changing anything:
$ git config --get core.sparseCheckout
true
$ git config --get core.sparseCheckoutCone
true
An absent output does not mean sparse checkout is broken, an unset configuration key simply prints nothing. Use the documented sparse-checkout commands when you actually intend to change the selected paths.
7. Verify the index after an operation
After anything that legitimately changes staging state, verify the result with Git rather than eyeballing timestamps:
$ git status --short
M readme.txt
$ git diff --cached --name-status
M readme.txt
$ git ls-files --stage -- readme.txt
100644 ce013625030ba8dba906f756967f9e9ca394464a 0 readme.txt
The first column in short status describes the index, and the cached diff shows what is actually staged. If you changed the index by mistake, git restore --staged -- path/to/file removes that one file from the index while keeping the working-tree file intact. Check the command's target carefully first, restoring a whole index or discarding working-tree changes is a different and potentially destructive action.
Done means
- You recorded the installed Git version and located the active index with
git rev-parse --git-path index. - You verified the
DIRCheader without modifying the file. - You used
git ls-filesto inspect entries, metadata and merge stages. - You treated extensions, sparse flags and shared-index files as managed state, not editable bytes.
- You know how to verify a deliberate staging change and how to unstage one path safely.