We've all done it a thousand times:
git init
And just like that, a .git directory appears. We add files, commit changes, push to remote—all without ever peeking inside that folder. It's the black box we trust but never examine.
Until today.
Last Tuesday, I was debugging a corrupted repository (don't ask), and I had no choice but to venture into the .git directory. What I found wasn't chaos or magic—it was a beautifully organized system that suddenly made everything about Git make sense.
Let me show you what's really in there.
Table of Contents
- The Moment of Truth: Opening .git
- The Directory Structure
- HEAD: Your Location Marker
- refs/: Where Branches Live
- objects/: Git's Time Machine
- index: The Staging Area Revealed
- config: Your Repository Settings
- hooks/: Automation Paradise
- logs/: Git's Diary
- The Files Git Doesn't Want You to See
- A Real-World Scenario: Following a Commit
- Why This Matters
- Commands to Explore Safely
- Key Takeaways
The Moment of Truth: Opening .git
Here's what most developers think .git contains: "Uh... git stuff?"
Let me show you what's actually in there. Open any git repository and run:
ls -la .git/
You'll see something like this:
.git/
├── HEAD
├── config
├── description
├── hooks/
├── index
├── info/
├── objects/
├── refs/
└── logs/
Each piece has a purpose. Each file tells a story. Let's decode them one by one.
The Directory Structure
Before we dive deep, here's the map:
| Path | What It Does |
|---|---|
HEAD |
Points to your current branch |
config |
Repository-specific settings |
description |
Used by GitWeb (rarely used) |
hooks/ |
Scripts that run on git events |
index |
Your staging area (binary file) |
info/ |
Global exclude patterns |
objects/ |
All your data lives here |
refs/ |
Pointers to commits (branches, tags) |
logs/ |
History of ref changes (reflog) |
The two most important directories? objects/ and refs/. Everything else is supporting infrastructure.
HEAD: Your Location Marker
Let's start simple. Check what's in HEAD:
cat .git/HEAD
Output:
ref: refs/heads/main
That's it. That's the file. HEAD is just a pointer telling Git which branch you're on. When you run git checkout feature-branch, Git rewrites this file:
ref: refs/heads/feature-branch
When you checkout a specific commit (detached HEAD state), it changes to:
a3f2d8b9c1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8
Mind-blowing realization #1: Your "current branch" is just a text file containing a reference.
refs/: Where Branches Live
Look inside the refs directory:
tree .git/refs/
.git/refs/
├── heads/
│ ├── main
│ ├── feature-auth
│ └── bugfix-login
├── remotes/
│ └── origin/
│ ├── main
│ └── develop
└── tags/
└── v1.0.0
Each branch is just a file containing a commit hash. Let's prove it:
cat .git/refs/heads/main
Output:
f3e4d5c6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2
That's your latest commit on main. That's literally what a branch is—a 40-character hash pointing to a commit.
When you create a new branch:
git branch feature-new
Git creates .git/refs/heads/feature-new and copies the current commit hash into it. That's it. No magic.
Mind-blowing realization #2: Branches are just files with commit hashes. Creating a branch is almost free because it's just writing 40 bytes to a file.
objects/: Git's Time Machine
This is where the real magic happens. The objects/ directory is Git's database—every commit, every file, every version you've ever created lives here.
ls .git/objects/
00/ 0f/ 1a/ 2b/ 3c/ 4d/ 5e/ 6f/ 7a/ 8b/ 9c/ ...
info/ pack/
Those two-character directories? They're organizing objects by their hash prefix (for performance). Let's look deeper:
ls .git/objects/a3/
f2d8b9c1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8
Combine the directory name with the filename: a3f2d8b9c1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8
That's a complete SHA-1 hash. But what is it?
What's a Git Object?
Git stores everything as objects. There are only four types:
- Blob - File contents
- Tree - Directory structure
- Commit - Snapshot in time
- Tag - Named reference (annotated tags)
You can inspect any object:
git cat-file -t a3f2d8b9 # Type
git cat-file -p a3f2d8b9 # Content
The Three Types of Objects
Let's see them in action. I'll create a simple file and commit it:
echo "Hello Git" > test.txt
git add test.txt
git commit -m "Add test file"
Now let's follow the trail.
1. The Commit Object
git cat-file -p HEAD
Output:
tree 5e8b9c0d1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6
parent f3e4d5c6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2
author John Doe <john@example.com> 1733654400 +0000
committer John Doe <john@example.com> 1733654400 +0000
Add test file
The commit points to a tree (the directory structure) and has metadata (author, timestamp, message).
2. The Tree Object
git cat-file -p 5e8b9c0d1a2b
Output:
100644 blob a3f2d8b9c1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8 test.txt
100644 blob 7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6 README.md
040000 tree b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7 src/
The tree lists files (blobs) and subdirectories (trees). It's like a snapshot of ls -l.
3. The Blob Object
git cat-file -p a3f2d8b9c1e4
Output:
Hello Git
The blob is just the file contents. No filename, no metadata—pure content.
Mind-blowing realization #3: Git doesn't store diffs. It stores complete snapshots. Every commit points to a full tree of all files.
Following the Chain
Here's how it connects:
HEAD
↓
refs/heads/main (contains: f3e4d5c6...)
↓
Commit f3e4d5c6...
├── tree 5e8b9c0d...
│ ├── blob a3f2d8b9... (test.txt)
│ ├── blob 7a8b9c0d... (README.md)
│ └── tree b9c0d1e2... (src/)
└── parent e2f3a4b5... (previous commit)
When you run git log, Git walks this chain backwards through parent commits.
index: The Staging Area Revealed
The staging area isn't virtual—it's a binary file:
file .git/index
Output:
.git/index: Git index, version 2, 3 entries
When you run git add, Git:
- Creates a blob object for the file
- Updates the index to point to that blob
- Doesn't create a commit yet
You can peek inside (it's binary, but git can read it):
git ls-files --stage
Output:
100644 a3f2d8b9c1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8 0 test.txt
100644 7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6 0 README.md
Each entry shows: permissions, blob hash, stage number, filename.
Mind-blowing realization #4: git add creates the object immediately. The staging area is just a list of "these blobs should be in the next commit."
config: Your Repository Settings
Open .git/config:
cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = git@github.com:username/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
[user]
name = John Doe
email = john@example.com
This is where git config --local writes settings. Global settings live in ~/.gitconfig, but repository-specific overrides live here.
Want to change your email for just this project?
git config user.email "work@company.com"
Check the file—it's been updated.
hooks/: Automation Paradise
The hooks/ directory contains scripts that run automatically on git events:
ls .git/hooks/
applypatch-msg.sample
pre-commit.sample
pre-push.sample
prepare-commit-msg.sample
post-commit.sample
...
Remove the .sample extension to activate a hook. For example, create .git/hooks/pre-commit:
#!/bin/bash
# Run tests before every commit
npm test
if [ $? -ne 0 ]; then
echo "Tests failed! Commit aborted."
exit 1
fi
Make it executable:
chmod +x .git/hooks/pre-commit
Now every git commit runs your tests first. If they fail, the commit is blocked.
Popular hooks:
-
pre-commit- Lint code, run tests -
commit-msg- Enforce commit message format -
pre-push- Run full test suite before pushing -
post-merge- Install dependencies after pulling
Mind-blowing realization #5: Git is programmable. You can automate almost anything.
logs/: Git's Diary
The logs/ directory tracks every change to refs:
cat .git/logs/HEAD
0000000 a3f2d8b John Doe <john@example.com> 1733654400 +0000 commit (initial): Initial commit
a3f2d8b f3e4d5c John Doe <john@example.com> 1733654500 +0000 commit: Add test file
f3e4d5c e2f3a4b John Doe <john@example.com> 1733654600 +0000 commit: Update README
This is what powers git reflog—a safety net that remembers where you've been, even if you've deleted branches or reset commits.
Accidentally deleted a branch?
git reflog
# Find the commit hash before you deleted it
git checkout -b recovered-branch a3f2d8b
The reflog saved you.
The Files Git Doesn't Want You to See
Some files are auto-generated and usually ignored:
- COMMIT_EDITMSG - Last commit message (for re-editing)
- FETCH_HEAD - Last fetched branch reference
- ORIG_HEAD - Backup of HEAD before risky operations
- MERGE_HEAD - Commit being merged (during conflicts)
- packed-refs - Compressed version of refs (for performance)
These files help Git track state during operations. They're temporary and rebuilt as needed.
A Real-World Scenario: Following a Commit
Let's put it all together. You run:
git commit -m "Fix login bug"
Here's what Git does behind the scenes:
- Creates blob objects for each file in the index
- Creates a tree object representing the directory structure
-
Creates a commit object pointing to:
- The tree
- The parent commit
- Author metadata
- Commit message
-
Updates the branch ref (
.git/refs/heads/main) to point to the new commit - Updates HEAD (if needed)
-
Logs the change in
.git/logs/refs/heads/main
All of this happens in milliseconds. And it's just file operations—no database, no network calls.
Why This Matters
Understanding .git changes how you use Git:
Before: "I'll just run these commands and hope it works."
After:
- "I need to recover a deleted branch—I'll check reflog."
- "I want to automate linting—I'll use pre-commit hooks."
- "This merge conflict makes sense—Git is comparing tree objects."
- "Branches are cheap—they're literally just pointers."
You stop fearing Git and start leveraging it.
Commands to Explore Safely
Want to dig deeper? These commands are read-only and safe:
# List all objects
find .git/objects -type f
# Show object type
git cat-file -t <hash>
# Show object content
git cat-file -p <hash>
# Show staging area
git ls-files --stage
# Show commit graph
git log --graph --oneline --all
# Show reflog
git reflog
# Show config
git config --list --show-origin
# Verify repository integrity
git fsck
Explore freely. You can't break anything by reading.
Key Takeaways
- Git is just files and directories - No magic, no mystery
- Branches are pointers - Creating them is nearly free
- Objects are immutable - Once created, they never change
-
The staging area is real - It's
.git/index - Everything is recoverable - Thanks to reflog and objects
- Git is hackable - Hooks let you automate workflows
- Commits store snapshots - Not diffs
The next time you run git init, remember: you're not just initializing a repository. You're creating a time machine, a database, and an automation system—all stored in plain files.
Have you ever peeked inside .git? What did you find? Drop a comment below—I'd love to hear your discoveries.
And if this helped demystify Git for you, share it with a fellow developer who's still treating .git like a black box. Let's spread the knowledge.
Want to go deeper? Check out Git Internals documentation or run git help hooks to see all available hooks.
Top comments (1)
Thanks never really looked into this. Well detailed