The Index: Git's Staging Area

Every Git user meets the staging area on day one — `git add` puts things there, `git commit` takes them out — but almost nobody knows what it actually is. The index is a real file, a binary snapshot-in-progress that sits between your working directory and the object store. Understanding it turns `add`, `reset`, and the difference between "staged" and "modified" from memorized rules into a picture you can see.

So far we have the object store (immutable snapshots) and refs (mutable pointers into them). But there’s a third region Git manages constantly, one that explains the two-step add-then-commit dance: the index, also called the staging area or the cache. This post is about what it is and why it exists.

Three trees

At any moment, Git is juggling three versions of your project — three “trees” in the loose sense:

Most Git confusion comes from forgetting the index exists and imagining only two states (committed vs. not). There are three, and the index is the one in the middle. git status is really a report on the differences between these three: “Changes to be committed” is HEAD-vs-index; “Changes not staged for commit” is index-vs-working-directory.

The index is a file

The index is not an abstraction — it is a concrete binary file at .git/index. It contains a sorted list of entries, one per tracked file, and each entry records the file’s path, its mode, a set of timestamps and size, and — crucially — the blob hash of the staged content.

Read that last part again: when you stage a file, Git writes the file’s content into the object store as a blob immediately and records that blob’s hash in the index. Staging is not “marking a file for later.” The content is hashed and stored the moment you git add. The index just remembers which blob represents each path in the pending commit.

You can see the index directly with the plumbing command git ls-files --stage:

$ git ls-files --stage
100644 a1b2c3d4... 0   README.md
100644 e5f6a7b8... 0   src/main.go

Mode, blob hash, stage number, path. That’s the staging area — a flat list mapping paths to already-stored blobs. When you commit, Git turns this flat list into tree objects (one per directory), writes them to the object store, and creates a commit pointing at the root tree. The index is the raw material a commit is built from.

Why git add after every edit

This clarifies a beginner’s frequent surprise: you edit a file you already staged, and Git says it’s both staged and modified. Now it makes sense. When you first git add foo, Git stored the then-current bytes as a blob and put that hash in the index. Your later edit changed the working-directory file but not the index entry. So:

git add again re-hashes the current content and updates the index entry to the new blob. The index always reflects a specific snapshot, frozen at the moment you last added — not a live view of your files. That’s a feature: it lets you commit exactly the version you inspected, even if you keep typing.

Staging is how you shape commits

Because the index is a separate, editable snapshot, you get precise control over what goes into each commit — independent of what you’ve changed on disk. This is the real payoff of a staging area, and it’s why Git has one when simpler systems don’t:

Moving things between the three trees

Most of the commands people find slippery are just “move data between HEAD, index, and working directory.” Seeing the target makes them obvious:

The --soft / --mixed / --hard trio, which people often memorize as magic, is just how far the reset reaches: the branch pointer only, then the index too, then the working directory as well. Three trees, three depths.

Key takeaways

Further reading

Sources & References

The three trees: HEAD, index, working directory