Understanding Git's Working Directory, Staging Area, and HEAD

Published  ·  LithiumGit Team  ·  10 min read

Almost every "why did Git do that?" moment traces back to the same gap: not knowing which of Git's three areas a command touches. Why does git diff go silent right after you run git add? Why does the same file show up twice in git status? Why does git reset --soft feel completely different from git reset --hard? Once you can picture the working directory, the staging area, and HEAD as three separate snapshots of your project, all of those answers become obvious — and they stay obvious for every Git command you meet later.

The Three Areas in One Picture

Git does not store one copy of your project — it maintains three at once. Each one holds a complete snapshot of every tracked file, and at any moment they can disagree with one another. Git's own documentation calls these the three trees.

1. Working DirectoryThe real files on your disk — the ones your editor opens and your compiler reads. This is the only area you can change directly.
2. Staging Area (the index)A draft of your next commit. It is not a list of filenames — it is a full snapshot, stored in the binary file .git/index. Nothing can be committed without passing through here first.
3. HEADA pointer to the commit at the tip of your current branch — in other words, the snapshot you committed last. HEAD is Git's answer to "where am I right now?"
Working Directorythe files on your diskapi.tsmodifiedapp.tsmodifiednotes.mduntrackedStaging Areathe .git/index fileapi.tsstagedyour next commit,as it stands right nowHEADyour last commitapi.tsapp.tscommitted & immutablegit addgit restoregit commitgit restore--staged

Content flows right to become permanent, and left to be thrown away. Every arrow overwrites its destination with the contents of its source.

💡 The one rule to rememberRightward commands (git add, git commit) promote your work toward permanence. Leftward commands (git restore) overwrite the left area with whatever the right area holds — which is why they can destroy work that only ever existed on the left.

The Working Directory — Where You Actually Work

Your working directory is simply your project folder: src/, package.json, everything you can see in your file explorer. Git checks it out for you when you switch branches, but after that it belongs to you and your editor. Git does not watch it, does not lock it, and does not back it up.

The one folder that is not part of your working directory is .git/. That is the repository itself: object database, refs, and the index. Delete .git/ and you keep your files but lose all history. Delete your files except .git/ and .git/ can restore every committed version.

Tracked, modified, staged, untracked

Git sorts every path in your working directory into one of a few states, and git status is just a report on that sorting:

  • Untracked — the file exists on disk but has never been added to the index or a commit. Git knows nothing about its contents.
  • Unmodified — the file on disk is byte-identical to the version in HEAD. Git stays quiet about it.
  • Modified — the file on disk differs from the staged version. The change exists only in your working directory.
  • Staged — the version in the index differs from HEAD. The change is queued for the next commit.
Terminal
# What differs between the working directory and the staging area?
git diff

# List every file that is currently presents in the git index
git ls-files

# Compact machine-readable status, including untracked files
git status --porcelain
⚠️ The only area with no safety netA commit can be recovered from the reflog. A staged snapshot can often be recovered from dangling blobs. But an edit that you never staged or committed exists only on disk, git restore and git reset --hard will erase it forever.

The Staging Area — Your Commit, Still in Draft

Most other version-control systems have nothing like the staging area: you commit, and your changes go straight into history. Git puts one step in between. That extra step lets you pick exactly which changes go into the commit, instead of committing everything you have edited.

Three names refer to the exact same thing: staging area, index, and cache. So when you see --staged in one command and --cached in another, both flags mean the same thing.

It is a snapshot, not a to-do list

git add does not mark a file for later. It copies the file's contents into the staging area straight away.

So if you edit the file after staging it, the staging area still holds the older version — and that is the version git commit will record.

Terminal
# Stage the current contents of a file
git add src/api.ts

# Stage only selected hunks — the real payoff of having a staging area
git add -p src/api.ts

# Review exactly what you are about to commit, before you commit it
git diff --staged

# Inspect the index itself: mode, blob hash, stage number, path
git ls-files --stage

# Unstage a file but keep your edits on disk
git restore --staged src/api.ts

Why bother staging at all?

  • Focused commits from unfocused work — fix a bug and a typo in one session, then commit them separately with git add -p.
  • A review step you controlgit diff --staged shows precisely what will be recorded, so debug prints and stray console.log lines get caught before they enter history.
  • Keeping local-only files out — scratch scripts and experiments can stay in the working directory indefinitely without ever being committed.
  • Conflict resolution — during a merge the index holds multiple stages of a conflicted file, and git add is how you tell Git "this version is resolved."
💡 About git commit -agit commit -a saves you a step: it stages your changes and commits them in one command. But it only includes files Git already tracks. A file you just created is left out.

HEAD — A Pointer, Not a Place

HEAD answers one question: which commit is my next commit's parent? Almost always it does so indirectly. .git/HEAD is a small text file, and its contents are usually not a commit hash at all but a reference to a branch:

Terminal
cat .git/HEAD
ref: refs/heads/main

cat .git/refs/heads/main
3a9f1c27b4e8d0a5f1c9b7e2d4a6c8e0f2b4d6a8

# The same chain, via plumbing commands
git symbolic-ref HEAD      # -> refs/heads/main
git rev-parse HEAD         # -> 3a9f1c27b4e8...

So the full chain is HEAD → branch → commit. That is what makes committing work: when you commit, Git creates the new commit and then moves your branch forward to point at it. HEAD itself never changes — it still says ref: refs/heads/main, and main now points somewhere new.

Normal stateHEAD → main → C3C1C2C3mainHEADDetached HEADHEAD → C2 directlyC1C2C3mainHEAD

Commits run left to right, oldest to newest. Normally HEAD points at a branch; when it points straight at a commit, HEAD is detached.

Detached HEAD, demystified

Run git checkout 3a9f1c2 and Git writes the raw hash into .git/HEAD instead of a ref: line. You can still make commits, but those commits have no branch pointing at them — move away and they become unreachable, which is exactly what the alarming warning message is trying to tell you. The fix is never dramatic: git switch -c my-branch creates a branch at your current position, or git switch main walks away.

HEAD-relative shorthand

NotationMeans
HEADThe commit at the tip of the current branch
HEAD~1 / HEAD~One commit back along the first-parent line
HEAD~3Three commits back along the first-parent line
HEAD^The first parent of HEAD (same as HEAD~1)
HEAD^2The second parent — only meaningful on a merge commit
HEAD@{2}Where HEAD pointed two moves ago, read from the reflog
ORIG_HEADWhere HEAD was before the last risky operation (merge, rebase, reset)
💡 ~ versus ^~ walks backward through generations; ^ chooses which parent at one commit. On a linear history they are interchangeable. On a merge commit, HEAD^1 is the branch you merged into and HEAD^2 is the branch you merged in.

Reading git status as Three Comparisons

git status looks like a list of files. It is really the result of two diffs and one filesystem scan, and its section headings map exactly onto the three areas:

Terminal
git status

On branch main

Changes to be committed:        # staging area vs HEAD
        modified:   src/api.ts

Changes not staged for commit:  # working directory vs staging area
        modified:   src/api.ts
        modified:   src/app.ts

Untracked files:                # on disk, in neither area
        notes.md

Note that src/api.ts appears in the first two sections at once. That is not a bug and it is not ambiguity — it is precise. Its staged snapshot differs from HEAD, and its on-disk contents differ from that staged snapshot. It was staged, then edited again.

⚠️ What would actually get committedIf you run git commit right now, only the staged version of src/api.ts is recorded — the later edits stay uncommitted, and src/app.ts is not included at all. Run git add again first if you want the newest contents.

The Three Diffs — Same Command, Different Pairs

With three areas there are three pairs to compare, and git diff gives you one invocation for each. This is the single most useful payoff of the mental model, and the answer to "why does git diff show nothing?"

Working DirectoryStaging AreaHEADgit diffgit diff --stagedgit diff HEAD

Three areas, three pairs, three diff invocations — each one compares a different pair.

CommandComparesAnswers
git diffWorking directory ↔ staging area"What have I changed but not staged?"
git diff --stagedStaging area ↔ HEAD"What exactly will my next commit record?"
git diff HEADWorking directory ↔ HEAD"What have I changed since my last commit, staged or not?"

So when git diff prints nothing right after git add, Git is not broken and your change is not lost. The working directory and the staging area now match, so there is genuinely no difference between them. Ask a different pair with git diff --staged and your change reappears.

Which Areas Does Each Command Touch?

Every everyday Git command can be described as "copy from area X into area Y." Once you can place a command on this table, its behaviour stops being something you memorise.

CommandWorking directoryStaging areaHEAD / branch
git add <file>UnchangedOverwritten from working directoryUnchanged
git commitUnchangedUnchangedNew commit; branch advances
git restore <file>Overwritten from staging areaUnchangedUnchanged
git restore --staged <file>UnchangedOverwritten from HEADUnchanged
git reset --soft HEAD~1UnchangedUnchangedBranch moves back one commit
git reset HEAD~1 (mixed)UnchangedOverwritten from new HEADBranch moves back one commit
git reset --hard HEAD~1Overwritten from new HEADOverwritten from new HEADBranch moves back one commit
git switch <branch>Updated to match target branchUpdated to match target branchPoints at the new branch
git stashCleaned to match HEADCleaned to match HEADUnchanged (changes saved aside)
git rm --cached <file>Unchanged (file stays on disk)Entry removed — file becomes untrackedUnchanged
💡 Why reset has three flagsgit reset <commit> moves your branch back to that commit. The flag decides what else changes: --soft stops there, --mixed also resets the staging area, and --hard resets your working directory too. Same operation, three depths. Give it file paths instead of a commit — git reset -- <file> — and it does something different: it only unstages those files and leaves your branch alone. Our guide to reset, revert, and restore covers that in detail.

Seeing the Three Areas in LithiumGit

A GUI helps here because it can show all three areas at once instead of one text report at a time. In LithiumGit, a free open-source Git GUI client, the changes panel splits staged and unstaged entries into separate lists, so the distinction the git status headings describe is visible at a glance rather than inferred from indentation.

Two lists: your working directory and your staging area

The Modified and Staged tabs are the first two areas. Below, two files have been edited and one of them has been staged — the counts Modified (2) and Staged (1) tell you at a glance how far each change has travelled.

LithiumGit changes panel showing a Modified tab with two files and a Staged tab with one file

Modified (2) is your working directory; Staged (1) is your staging area

Each list diffs against a different area

This is where the three-area model pays off, because LithiumGit names the pair it is comparing in the title bar. Select a file from Modified and you get Index ↔ Working Directory — the same comparison as git diff. Select one from Staged and you get HEAD ↔ Index — the same comparison as git diff --staged.

LithiumGit diff view titled Index versus Working Directory, opened from the Modified tab

From Modified — Index ↔ Working Directory, i.e. git diff

LithiumGit diff view titled HEAD versus Index, opened from the Staged tab

From Staged — HEAD ↔ Index, i.e. git diff --staged

Notice that both screenshots show the same file and the same change. Only the areas differ.

HEAD in the commit graph

The third area is a pointer, so LithiumGit draws it as one. In the graph the current commit is marked H for HEAD, with the branches that point at it listed above.

LithiumGit commit graph with the current commit marked H for HEAD and the master and origin/master branch labels above it

The commit marked H is where HEAD points; master and origin/master point at the same commit

Five Gotchas That Suddenly Make Sense

  • A file listed as both staged and unstaged. You staged it, then kept editing. The index and your disk hold two different versions, and both differ from HEAD.
  • .gitignore being ignored. Ignore rules only apply to untracked paths. If a file is already in the index, Git keeps tracking it — you need git rm --cached <file> to evict it from the staging area first.
  • An empty git diff on a real change. The change is staged, so the working directory and the index agree. Use git diff --staged.
  • git commit -a missing a new file. The -a flag only auto-stages tracked files. An untracked file needs an explicit git add.
  • Committing on a detached HEAD and "losing" the commit. No branch advanced, so nothing references your new commit once you switch away. Find it with git reflog and attach a branch to it.
💡 A three-question habitBefore running anything unfamiliar, ask: which areas does this read? Which does it overwrite? Does anything it overwrites hold work that exists nowhere else? If the answer to the third question is yes, commit or stash first.

Frequently Asked Questions

What is the difference between the working directory, the staging area, and HEAD in Git?
The working directory is the set of actual files on your disk that you edit. The staging area (also called the index) is a snapshot of what your next commit will contain, stored in the .git/index file. HEAD is a pointer to the commit at the tip of your current branch — the last committed snapshot. Content moves right with git add and git commit, and left with git restore.
Is the staging area the same thing as the Git index?
Yes. The staging area, the index, and the cache are three names for the same thing: the binary file at .git/index that records the exact content and metadata of every path that will go into your next commit. That is why some commands say 'staged' while some flags are named --cached.
What exactly does HEAD point to in Git?
HEAD is normally a symbolic reference to a branch rather than directly to a commit. The file .git/HEAD contains a line like 'ref: refs/heads/main', and that branch file contains the commit hash. When HEAD contains a raw commit hash instead, you are in a detached HEAD state.
Why does git diff show nothing after I run git add?
Plain git diff compares the working directory against the staging area. Once you stage a file those two are identical, so there is nothing to report. Use git diff --staged to compare the staging area against HEAD, or git diff HEAD to compare the working directory against the last commit.
Why does the same file appear under both 'Changes to be committed' and 'Changes not staged for commit'?
Because you staged the file and then edited it again. The staged version differs from HEAD, which puts it under 'Changes to be committed', and the version on disk now differs from the staged version, which also puts it under 'Changes not staged for commit'. Committing now would record the staged version, not what is on disk.
What does the staging area actually give me?
It lets you build a commit deliberately instead of committing whatever happens to be on disk. You can stage part of a file with git add -p, split one messy editing session into several focused commits, and review exactly what is about to be recorded with git diff --staged before you commit.
Does git commit -a skip the staging area?
No — it fills the staging area for you automatically, then commits. It stages every modification and deletion to already-tracked files only, so a brand-new untracked file will not be included unless you git add it first.
Is the .git folder part of my working directory?
No. The .git folder is the repository itself: the object database, the refs, and the index. Your working directory is everything else in the project folder — the files you edit. Deleting .git leaves your files with no history; deleting your files leaves .git able to restore every committed version.
What is the difference between HEAD~1 and HEAD^2?
HEAD~1 walks one generation back along the first-parent line. HEAD^2 selects the second parent of HEAD, which only exists on a merge commit. On a linear history HEAD~1 and HEAD^1 mean the same commit; on a merge commit HEAD^1 is the branch you merged into and HEAD^2 is the branch you merged in.