Skip to main content
RunBook Academy

Git, CI/CD & GitOpsII · Git ArchitectureArchitecture

Environment variables and config files — how Git finds its repository and its settings

Intermediate⏱ ~20 mingit

What you'll learn

  • Trace the path Git takes to locate the repository, the index, the working tree, and the config
  • Use GIT_DIR, GIT_WORK_TREE, GIT_OBJECT_DIRECTORY, GIT_INDEX_FILE, and GIT_DIR with intent
  • Apply the three-level config cascade (system, global, local) to debug unexpected behaviour
  • Recognise when a misused environment variable produces silent corruption
  • Choose between env vars and the config cascade for a given deployment scenario

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

Every Git command answers three questions before it does anything: “where is the repository?”, “where is the index?”, “where is the working tree?”. The answers come from environment variables and from the directory layout. Most of the time the defaults are correct and you never think about it. When you script Git against an arbitrary repository, run multiple Git operations in parallel without index contention, or debug a “why is Git using the wrong config?” incident, the environment variables and the config cascade are the answer.

How Git finds the repository

flowchart TB
    A["GIT_DIR set?"] -->|yes| B["use $GIT_DIR"]
    A -->|no| C["--git-dir CLI flag?"]
    C -->|yes| D["use --git-dir"]
    C -->|no| E["Walk up from cwd\nlooking for .git/"]
    B --> F["Repository located"]
    D --> F
    E --> F

The lookup order is:

  1. GIT_DIR environment variable.
  2. --git-dir command-line flag.
  3. Walk up from the current directory looking for .git/.
  4. If none found, fail with fatal: not a git repository.

For a bare repository, GIT_DIR points at the repository itself (not at a .git subdirectory). For a non-bare repository, it points at the .git directory.

The working tree is found by a similar cascade: GIT_WORK_TREE environment variable, then --work-tree flag, then the parent directory of GIT_DIR.

The five environment variables that matter

env | grep ^GIT_
flowchart LR
    A["GIT_DIR"] --> A1[".git location or bare repo path"]
    B["GIT_WORK_TREE"] --> B1["working tree root"]
    C["GIT_OBJECT_DIRECTORY"] --> C1["extra objects/ directory"]
    D["GIT_INDEX_FILE"] --> D1["replacement for .git/index"]
    E["GIT_DIR"] --> E1["alias for GIT_DIR in some contexts"]
  • GIT_DIR — the path to the repository (the .git directory for a non-bare repo, or the repo root for a bare repo). Setting this is the standard way to point Git at an arbitrary repository.
  • GIT_WORK_TREE — the path to the working tree. Defaults to the parent of GIT_DIR for non-bare repos. Set this when the working tree is not the parent of .git/.
  • GIT_OBJECT_DIRECTORY — an additional objects/ directory Git will search when looking for an OID. Useful for sharing objects between repositories without copying them.
  • GIT_INDEX_FILE — a replacement for .git/index. The single most useful variable for scripted Git operations, because it lets you stage a commit in a temporary index without touching the user’s real index.
  • GIT_DIR — an alias for the same purpose as GIT_DIR in some scripts; the underlying value is the same.

Where each is useful

# 1. Point Git at a repository in a non-standard location
GIT_DIR=/var/lib/repos/prod.git git --git-dir=/var/lib/repos/prod.git log --oneline -5

# 2. Operate against a bare repo with no working tree
GIT_DIR=/var/lib/repos/prod.git git --bare log --oneline -5

# 3. Use a temporary index to stage a commit without touching the user's index
TMP_IDX=$(mktemp)
GIT_INDEX_FILE="$TMP_IDX" git read-tree HEAD
GIT_INDEX_FILE="$TMP_IDX" git update-index --add --cacheinfo 100644,"$BLOB",newfile
TREE=$(GIT_INDEX_FILE="$TMP_IDX" git write-tree)
rm -f "$TMP_IDX"

# 4. Read additional objects from a shared store
GIT_OBJECT_DIRECTORY=/srv/git-shared/objects git log --oneline

The config cascade

Git configuration is resolved from three levels, merged in order (lowest priority first):

flowchart TB
    S["System\n/etc/gitconfig"] --> M["Merged"]
    G["Global\n~/.gitconfig or ~/.config/git/config"] --> M
    L["Local\n.git/config"] --> M
    W["Worktree\n.git/config.worktree"] --> M
    M --> R["Final effective value"]

Inspect the cascade with:

git config --list --show-origin
# system  /etc/gitconfig   user.name=git
# global  ~/.gitconfig     user.email=ci@example.com
# local   .git/config      user.email=eng@example.com

The effective value for user.email is eng@example.com — local overrides global overrides system. For an infrastructure engineer, this is the right place to set per-repository identities (a shared service repo can have user.email=ops@example.com baked into its .git/config via git config user.email ops@example.com), without affecting the engineer’s personal global config.

Operational recipes

Sandbox an index for a CI run

# Use a per-step index to avoid contention between parallel jobs
GIT_INDEX_FILE="$JOB_STAGE/.tmp-index" git read-tree HEAD
GIT_INDEX_FILE="$JOB_STAGE/.tmp-index" git --work-tree="$WORK_TREE" \
    update-index --add --cacheinfo 100644,"$BLOB",path/to/file
GIT_INDEX_FILE="$JOB_STAGE/.tmp-index" git --work-tree="$WORK_TREE" \
    diff --cached

Inspect a bare repository with no working tree

GIT_DIR=/var/lib/repos/prod.git git --git-dir=/var/lib/repos/prod.git \
    log --oneline -10

Move a ref without a working tree

GIT_DIR=/var/lib/repos/prod.git git --git-dir=/var/lib/repos/prod.git \
    update-ref refs/heads/main $COMMIT_OID

Disable a hook for a one-off operation

GIT_DIR=/path/to/repo GIT_HOOKS=/dev/null git commit -m "bypass hooks"

Setting GIT_HOOKS to a directory with no executable hooks (or to /dev/null on a system that tolerates it) is the cleanest way to skip hooks without setting core.hooksPath in config.

Production discipline

  1. Always set GIT_DIR explicitly in scripts. A script that infers the repository from cwd will break when the script is invoked from a different directory. Set GIT_DIR (or use --git-dir) at the top of the script and echo git rev-parse --git-dir to verify.
  2. Use a temporary GIT_INDEX_FILE for staged operations in CI. A long-running CI pipeline that shares an index across steps risks contention; a per-step index isolates each step and makes the staged content auditable.
  3. Treat the config cascade as a debugging tool. When a command is “doing the wrong thing”, git config --list --show-origin is the first diagnostic. The output shows exactly which file is setting the offending value.

Cross-course references

  • Linux for Production Sysadmins - Part XXXII (Shell) covers the analogous cascade for shell configuration: /etc/ profile, ~/.bashrc, and per-script sourcing. The three-level pattern is the same.
  • Ansible for Production Sysadmins - Part IX (Inventory) uses the same cascade: ANSIBLE_CONFIG (env), ./ansible.cfg (cwd), and ~/.ansible.cfg (home). The reasoning is the same — explicit override, scoped defaults, and a predictable resolution order.
  • Kubernetes for Production Sysadmins - Part XXIV (GitOps) uses GIT_DIR and GIT_WORK_TREE semantics in the GitOps controller to reason about the syncing boundary between the declared state (HEAD) and the live state (working tree).

Quiz

Knowledge check · 4 questions

  1. Q1. A CI step wants to stage a commit in a fresh index without disturbing the engineer's working index. Which environment variable is the right tool?

  2. Q2. An engineer sets `GIT_DIR=/var/lib/repos/prod.git` and runs `git log --oneline -5` from a different working directory. Git will walk up from the cwd to find the repository.

  3. Q3. Name the three levels of Git's config cascade and the path each looks at by default.

  4. Q4. Diagnose why a CI pipeline is committing with the wrong identity, and identify the fix.

    A shared CI pipeline runs on a Linux runner as the `ci` user. The pipeline is supposed to commit using `ci-bot@example.com`, but the commits are being authored by the engineer's personal email. The pipeline sets `GIT_DIR` to the target repository but does not set `user.email` explicitly. The CI user's `~/.gitconfig` is empty.

Passing score: 75%. Answers are checked in this browser.