git init

git init is the command that turns an ordinary folder into a Git repository. It is almost always the very first command you run on a new project, and it is also the command you run to start tracking history in a folder that already has files in it. Understanding exactly what git init creates — and what it does not do — is the foundation for everything else you will learn about Git.

Overview / How it works

Every Git repository is really just a hidden directory named .git sitting alongside your project files. That directory is a self-contained database: it stores every version of every file you commit, the branches you create, and the configuration for that one repository. When you run git init inside a folder, Git creates this .git directory and nothing else — your existing files are left completely untouched and are not yet tracked.

Inside a freshly initialized .git directory you will find several important pieces:

  • .git/objects/ — the object database. This is where Git will eventually store blobs (the raw content of files), trees (snapshots of a directory, mapping file names to blobs and sub-trees), and commits (a pointer to one tree, plus a commit message, author, timestamp, and a pointer to the parent commit or commits). Every object is identified by the SHA-1 hash of its content, so two files with identical content are stored only once, no matter how many commits or branches reference them.
  • .git/refs/ — where branches and tags live. A branch, such as main, is nothing more than a small text file containing the SHA-1 of the commit it currently points to. Branches are cheap and fast in Git precisely because creating one just writes a 41-byte pointer — it never copies files.
  • .git/HEAD — a pointer to the branch you currently have checked out. Right after git init, HEAD is a symbolic reference like ref: refs/heads/main, even though the main branch does not exist as a real ref yet — it only comes into existence once you make your first commit.
  • .git/config — settings that apply only to this repository (remotes, your name/email overrides, merge and rebase behavior, and so on). This is what git config --local edits, as opposed to --global (per-user) or --system (machine-wide) config.
  • .git/index — the staging area, sometimes called the “index” or “cache”. This file does not exist immediately after git init; it is created the first time you run git add. The index is what sits between your working directory and your next commit — it holds a snapshot of exactly what will go into the next commit, which is why git add and git commit are two separate steps.

Because git init only creates this database and never modifies or deletes any of your project files, it is a very safe command to run. It is also idempotent: running git init again in a directory that is already a repository does not destroy your history — Git simply reports that it re-initialized the existing repository and leaves your commits, branches, and config alone.

Syntax

git init [options] [directory]
Flag Description
(no arguments) Initializes a repository in the current directory.
<directory> A path to create and initialize instead of the current directory. Git creates the directory if it does not already exist.
-b <name>, --initial-branch=<name> Sets the name of the initial branch (what HEAD will point to) instead of relying on the default. Available since Git 2.28.
--bare Creates a repository with no working directory — only the contents of what would normally be the .git folder, placed directly in the target directory. Used for repositories that only serve as a remote (a push/pull target), not a place to edit files.
-q, --quiet Suppresses the “Initialized empty Git repository…” output, useful in scripts.
--template=<dir> Copies files from a custom template directory into the new .git directory (for example, default hooks) instead of Git’s built-in templates.

Since Git 2.28 you can also avoid depending on the -b flag every time by setting a global default branch name once:

git config --global init.defaultBranch main

Without this, older Git versions default new repositories to a branch named master, which is why explicitly naming it or configuring the default is worth doing on any machine you set up.

Examples

Example 1: Starting a brand-new project

mkdir hello-git
cd hello-git
git init

Output:

Initialized empty Git repository in /home/user/hello-git/.git/

This is the most common way to use git init: create an empty folder, move into it, and initialize it. At this point there are no commits and no branch yet — running git status would report “No commits yet” and list any files in the directory as untracked.

Example 2: Turning an existing project into a Git repository

You do not need to start from an empty folder. If you already have a project with files in it and just haven’t started tracking it with Git, run git init right where the files are:

cd my-existing-project
git init
git add .
git commit -m "chore: initial commit"

Output:

Initialized empty Git repository in /home/user/my-existing-project/.git/
[main (root-commit) 3f9a1c2] chore: initial commit
 4 files changed, 128 insertions(+)
 create mode 100644 README.md
 create mode 100644 index.html
 create mode 100644 src/app.js
 create mode 100644 style.css

git init only creates the empty repository — it does not stage or commit anything on its own. The git add . stages every file in the directory into the index, and git commit takes that snapshot and creates the very first commit object, which is also when the main branch ref is actually written to .git/refs/heads/main for the first time.

Example 3: Setting the branch name explicitly and inspecting the result

git init --initial-branch=main project-api
cd project-api
ls -a
cat .git/HEAD

Output:

Initialized empty Git repository in /home/user/project-api/.git/
.  ..  .git
ref: refs/heads/main

Here git init is given a directory name as an argument, so Git creates project-api/ first and initializes the repository inside it — you don’t have to mkdir and cd separately. The --initial-branch=main flag guarantees the branch will be called main even on a machine where init.defaultBranch was never configured. Note that ls -a shows only the .git folder — no other project files exist yet — and .git/HEAD already contains a symbolic reference to refs/heads/main, even though that ref file doesn’t exist until the first commit.

How it works step by step

When you run plain git init, Git performs a small, well-defined sequence of steps:

  • It checks whether a .git directory already exists in the target location. If one does, it reuses it (re-initializes) instead of overwriting your history.
  • It creates the .git directory and, inside it, the subdirectories objects/, refs/heads/, and refs/tags/.
  • It writes a HEAD file containing ref: refs/heads/<branch-name>, using either -b/--initial-branch, your configured init.defaultBranch, or Git’s built-in fallback name.
  • It writes a default config file (repository-local settings) and a description file (used by some old Git web tools).
  • It copies default hook scripts (currently disabled, with a .sample suffix) into .git/hooks/, such as pre-commit.sample and commit-msg.sample.
git init demo-repo
ls demo-repo/.git
Initialized empty Git repository in /home/user/demo-repo/.git/
HEAD  config  description  hooks  info  objects  refs

Nothing in your working directory changes, no object is written to objects/ yet (that happens on your first git add), and no branch ref exists yet in refs/heads/ (that happens on your first commit). git init only lays the empty scaffolding.

Common Mistakes

Mistake 1: Running git init inside an already-tracked project

cd my-project/src/components
git init

If my-project is already a Git repository, running git init again in a subdirectory creates a second, nested repository with its own .git folder. Git will then treat that subdirectory as an embedded repository, and files inside it can silently stop showing up in git status or git add for the parent repo, which is confusing and hard to notice until something goes missing from a commit. Before running git init, always check whether you’re already inside a repository:

git rev-parse --is-inside-work-tree

If that prints true, you’re already tracked and should not run git init again in a subfolder. If you genuinely need to nest one project inside another under version control, use git submodule instead of an accidental nested .git.

Mistake 2: Initializing in the wrong directory (like your home folder)

cd ~
git init

Running git init in your home directory, or any directory far broader than your intended project, turns every file underneath it into a potential candidate for tracking. It’s easy to later run git add . without realizing you’re standing several levels above your actual project, staging unrelated files, caches, or even other repositories. Always confirm your location with pwd before initializing, and prefer passing an explicit directory argument (git init my-project) so the target is unambiguous rather than relying on wherever your shell happens to be.

Mistake 3: Forgetting to set a consistent default branch name

Different machines and different Git versions can default a new repository’s first branch to master instead of main, especially on older installs. A team where some members’ Git defaults to master and others’ to main ends up with inconsistent branch names across otherwise-identical setups. Fix this once per machine with git config --global init.defaultBranch main, or make it explicit per-repository with git init -b main.

Best Practices

  • Set init.defaultBranch globally once per machine so every new repository starts on main without needing the -b flag each time.
  • Run git rev-parse --is-inside-work-tree (or just check with git status) before calling git init if you’re unsure whether the current folder is already tracked.
  • Pass a directory argument (git init my-app) instead of mkdir && cd && git init when you want to create and initialize a new project folder in one step.
  • Add a .gitignore file right after git init, before your first git add, so build artifacts and dependency folders never get staged in the first place.
  • Follow up git init with a first commit quickly (even an empty README.md) so the main branch ref actually exists and collaborators have a stable point to branch from.
  • Use --bare only for repositories meant to act purely as a remote (for example, one you host yourself); never try to edit files directly inside a bare repository’s working area, because it has none.

Practice Exercises

  • Exercise 1: Create a new folder called notes-app, initialize it as a Git repository with the initial branch explicitly named main, and confirm the branch name by inspecting .git/HEAD. What does .git/HEAD contain before you make any commit?
  • Exercise 2: You have a folder called legacy-scripts containing five files that have never been under version control. Initialize it as a Git repository, stage all the files, and make a single commit with a Conventional Commits-style message. Then check git log to confirm the commit exists.
  • Exercise 3: Run git rev-parse --is-inside-work-tree inside a folder you’ve already initialized, and then inside one you haven’t. Explain, in your own words, why checking this before running git init again would have prevented Mistake 1 above.

Summary

  • git init creates a .git directory that turns a plain folder into a Git repository; it never touches or commits your existing files.
  • The .git directory holds the object database (objects/), branch and tag pointers (refs/), the current branch pointer (HEAD), repository-local settings (config), and the staging index once you start adding files.
  • A branch is just a small file pointing at a commit’s SHA-1 hash; right after git init, HEAD points at a branch name that doesn’t exist as a ref yet, until your first commit creates it.
  • Use -b/--initial-branch or git config --global init.defaultBranch main to make sure new repositories consistently start on main.
  • Use --bare only for remote-only repositories with no working directory, and check git rev-parse --is-inside-work-tree before re-running git init to avoid creating accidental nested repositories.