Git Stash: Untracked Files Follow Branches – CI Failures
git stash ignores untracked files.
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
- ✓Basic programming fundamentals
- ✓A computer with internet access
- ✓Willingness to follow along with examples
- git stash push -m 'description': saves tracked changes with a label
- git stash -u: includes untracked files (new files not yet git add'd)
- git stash pop: restores and removes from stack — use when confident no conflicts
- git stash apply: restores but keeps in stack — use when conflicts are possible
Git stash is a local-only mechanism that temporarily shelves uncommitted changes in your working directory, letting you switch branches or contexts without committing half-baked work. It solves the immediate problem of needing to pivot—say, to fix a production bug while mid-feature—without losing your progress or creating messy WIP commits.
The stash lives as a stack of commits in .git/refs/stash and its reflog, not in any remote, meaning it's purely local and ephemeral by design. The trap? By default, git stash only stashes tracked files—untracked and ignored files stay put, silently following you across branches and causing CI failures when stale build artifacts or configs pollute a different branch's environment.
Alternatives like git worktree let you maintain multiple working directories simultaneously, avoiding the stash entirely, while git commit --allow-empty or temporary branches offer more durable context switches. For untracked files, you need git stash -u or git stash --include-untracked to capture them, or git stash --all to also grab ignored files—but use the latter with caution, as it can wipe build caches.
The stash is a convenience tool, not a safety net; it's best for short-lived interruptions, not long-term storage, and it's a common source of subtle bugs when developers assume it captures everything.
Git stash is a clipboard for your uncommitted work. You're mid-feature when someone drops an urgent bug on your desk. Instead of committing half-finished code or losing your changes, you stash them — Git packs up your working directory changes into a temporary storage area, restores you to a clean HEAD, and waits. Fix the bug, deploy the hotfix, then come back and unstash. Your work resumes exactly where you left it.
git stash shelves uncommitted working directory and index changes into a stack. It is the standard tool for context switching — you stash your current work, switch to a hotfix branch, then unstash when you return. The core failure mode: stash pop deletes the stash even on merge conflicts, leaving you with no backup. Use apply + manual drop when conflicts are possible.
The second failure mode: stash does not include untracked files by default. New files you have not git add'd remain in the working directory when you switch branches. They get committed to the wrong branch. Use stash -u to include them.
Stashes are global to the repository, not scoped to a branch. You can pop a stash made on feature/x onto main. This is sometimes intentional and sometimes a very bad day. Always use stash push -m 'description' to label your stashes — unnamed stashes become mystery boxes within 24 hours.
What Is Git Stash — The Fundamentals
git stash temporarily shelves uncommitted changes, restoring your working directory to match the last commit. This lets you switch branches, apply a hotfix, or start a different task without committing half-finished work.
Core commands: - git stash push -m 'describe your changes' — save changes with a label - git stash list — view all saved stashes (shows label, branch, date) - git stash pop — restore the latest stash and remove it from the stack - git stash apply — restore without removing (safer when conflicts possible) - git stash drop — remove a stash without applying - git stash push -u or --include-untracked — CRITICAL: also saves new (untracked) files - git stash push -a or --all — saves everything including ignored files
The stack: Stashes are stored in a LIFO (last in, first out) stack. stash@{0} is the most recent. You can reference specific stashes: git stash pop stash@{2}.
Critical behavior: - Stash only saves TRACKED files by default. New files not yet git add'd are NOT included. - git stash pop removes the stash from the stack EVEN if the apply hits a merge conflict. This is the #1 cause of stash-related work loss.
# Start with a clean repo git init && echo 'Initial' > tracked.txt && git add . && git commit -m 'Initial' # Make some changes echo 'Half-finished feature' >> tracked.txt echo 'New file not added yet' > untracked.txt # Stash only tracked changes (untracked.txt stays!) git stash push -m 'WIP: feature in progress' git status # tracked.txt is clean, but untracked.txt is still here! # Stash with untracked files git stash push -u -m 'WIP: with new files' git status # now everything is clean # See all stashes git stash list # Restore the latest stash git stash pop # If conflict possible: use apply instead git stash apply stash@{0} git stash drop stash@{0}
git stash pop removes the stash from the stack even if the apply hits a merge conflict. If you can't resolve the conflict, the stash is gone but the changes are not applied. Always use git stash apply when there's any chance of conflict (e.g., you're popping on a different branch). Then git stash drop manually after verifying.git switch -c temp/wip && git add . && git commit -m 'WIP: stash replacement' — this is safer than stash because it's a real branch with full recovery options. Stash has no merge conflict resolution UI; a branch can be merged properly.-u to include untracked files. Use apply (not pop) when conflicts are possible. For long-term context switching, use a WIP branch instead — it's safer and has full merge support.Git Stash: The Hidden Trap of Untracked Files
Git stash temporarily shelves changes in your working directory, letting you switch branches without committing half-finished work. The core mechanic: git stash records tracked file modifications and staged changes, then reverts your working tree to match HEAD. But by default, it ignores untracked and ignored files — a detail that routinely causes CI failures.
When you stash, Git creates a stash entry as a special commit-like object reachable via stash@{0}. Crucially, the stash is not branch-specific; it lives on the refs/stash reflog. Applying a stash merges its changes into the current working tree, which can produce conflicts if the branch has diverged. Untracked files remain untouched unless you explicitly use git stash -u or git stash --include-untracked.
The practical danger: a developer stashes changes, switches to a feature branch, builds, and commits — but untracked files (like generated configs or IDE artifacts) from the original branch remain in the working directory. Those files get committed, polluting the branch and breaking CI builds that expect a clean environment. Use git stash -u when you need a truly clean slate, or git stash --all to also clear ignored files.
git stash leaves untracked files behind. Always use git stash -u if you need to guarantee no stray files follow you to another branch.git status and explicitly stash untracked files with -u if any are present.git stash does not touch untracked or ignored files — they persist across branches.git stash -u (or --all) when you need a fully clean working directory.Basic Stash: Save and Restore Work
The core workflow is three commands. git stash shelves everything in your working directory and index that differs from HEAD. git stash pop restores the most recent stash and removes it from the stack. git stash apply restores without removing — useful when you want to apply the same stash to multiple branches.
One thing that surprises developers coming from other VCS systems: git stash by default only stashes tracked files. If you created a new file that hasn't been git add-ed yet, it won't be stashed. It'll sit there in your working directory when you switch branches, which can lead to unintended commits on the wrong branch. Use git stash -u (or --include-untracked) to include new files.
The stash is a stack — each git stash pushes a new entry. git stash pop pops the top (most recent). This is correct behaviour for the common case but becomes confusion when you stash, do some more work, stash again, and then try to figure out which stash has what.
# io.thecodeforge — Git Stash Basics # ───────────────────────────────────────────────────────────── # Save current work to stash (tracked files only) # ───────────────────────────────────────────────────────────── git stash # Save with a descriptive message — always do this git stash push -m "WIP: payment retry exponential backoff" # Include untracked files (new files not yet git add'd) git stash push -u -m "WIP: new PaymentRetryService class" # ───────────────────────────────────────────────────────────── # Restore most recent stash AND remove it from the stack # ───────────────────────────────────────────────────────────── git stash pop # Restore most recent stash but KEEP it in the stack git stash apply # ───────────────────────────────────────────────────────────── # List all stashes # ───────────────────────────────────────────────────────────── git stash list # Output: # stash@{0}: On feature/payment-retry: WIP: payment retry exponential backoff # stash@{1}: On main: WIP: hotfix debug logging # ───────────────────────────────────────────────────────────── # Restore a specific stash by index # ───────────────────────────────────────────────────────────── git stash pop stash@{1} git stash apply stash@{1} # ───────────────────────────────────────────────────────────── # See what's in a stash before applying # ───────────────────────────────────────────────────────────── git stash show stash@{0} git stash show -p stash@{0} # Full diff
- Stash is a stack — push adds, pop removes the top
- Stashes are global — not attached to any branch
- You can pop a stash made on feature/x onto main (or any branch)
- This flexibility is powerful but dangerous — always check which branch you are on before popping
Stash Pop vs Apply: When to Use Each
The difference matters more than most tutorials let on.
git stash pop = apply + delete from stack. Use this 95% of the time. The moment you successfully restore your work, the stash entry is gone. Clean, no accumulation.
git stash apply = apply but leave in stack. Use this when: you want to apply the same changes to multiple branches (e.g. a config change you need on both a feature branch and a hotfix branch), or when you're not 100% sure the stash applies cleanly and you want to keep it as a backup while you verify.
There's one critical behaviour difference: if git stash pop encounters a conflict, it still removes the stash from the stack before throwing the conflict. You're now in a conflicted state with the stash gone. This is a Git footgun — you want git stash apply when there's any chance of conflicts, so the stash survives even if the apply fails.
# io.thecodeforge — Stash Pop vs Apply # ───────────────────────────────────────────────────────────── # Scenario: you might have conflicts — use apply, not pop # ───────────────────────────────────────────────────────────── git stash apply stash@{0} # If it applied cleanly with no conflicts, manually drop the stash git stash drop stash@{0} # If there ARE conflicts, resolve them, then drop manually # The stash is still in the stack — you haven't lost anything git status # Shows conflicted files # ... resolve conflicts ... git add . git stash drop stash@{0} # Now safe to remove # ───────────────────────────────────────────────────────────── # Apply same stash to two branches # ───────────────────────────────────────────────────────────── git checkout feature/branch-a git stash apply stash@{0} # Applied to branch A — stash still exists git checkout feature/branch-b git stash apply stash@{0} # Applied to branch B too git stash drop stash@{0} # Now clean up # ───────────────────────────────────────────────────────────── # Clear ALL stashes (nuclear — use with care) # ───────────────────────────────────────────────────────────── git stash clear
Stashing Partial Changes and Specific Files
Sometimes you only want to stash part of your changes — for example, you've been working on two unrelated things in the same branch (happens to everyone, don't judge) and you want to commit one thing cleanly before stashing the other.
git stash push -- <path> stashes only the specified files. Everything else stays in your working directory.
git stash push -p (patch mode) drops you into an interactive hunk-by-hunk selection — exactly like git add -p. You choose yes/no for each chunk. This is powerful but slow for large changesets.
# io.thecodeforge — Stashing Partial Changes and Specific Files # ───────────────────────────────────────────────────────────── # Stash only specific files # ───────────────────────────────────────────────────────────── git stash push -m "WIP: retry config" -- src/main/java/io/thecodeforge/payment/RetryConfig.java # Stash multiple specific files git stash push -m "WIP: two related files" \ -- src/main/java/io/thecodeforge/payment/RetryConfig.java \ -- src/main/java/io/thecodeforge/payment/RetryPolicy.java # ───────────────────────────────────────────────────────────── # Interactive patch mode — choose which hunks to stash # ───────────────────────────────────────────────────────────── git stash push -p -m "WIP: partial changes" # Git shows each change hunk and asks: stash this hunk? [y/n/q/a/d/?] # ───────────────────────────────────────────────────────────── # Create a branch from a stash (useful for long-lived stashes) # ───────────────────────────────────────────────────────────── git stash branch feature/retry-config stash@{0} # Creates new branch at the commit where you stashed, applies the stash, drops it
- git stash branch <name> <stash>: creates branch at stash point, applies stash, drops it
- Use when a stash has been sitting for days and has become important work
- Branches are visible in git branch -a. Stashes are only visible in git stash list.
- If a stash survives for weeks, it should have been a branch from the start.
Git Stash vs Git Reset: The Nuclear Option vs The Snooze Button
Junior devs treat git stash like a magical undo button. It's not. Let's kill this confusion right now.
Git reset --hard throws away your working directory changes permanently. That's the nuclear option. Git stash temporarily shelves changes so you can pick them up later. That's the snooze button.
When you're mid-refactor and a critical production bug drops in your lap, you stash your messy working tree, fix the bug, then pop the stash. If you had used reset, those refactored files are gone. Forever.
Here's the real-world distinction: reset moves the HEAD pointer and optionally overwrites the index and working tree. Stash is a stack of compressed delta objects stored in .git/refs/stash. Reset is destructive by design. Stash is a save-state for your incomplete work.
If you're using git stash as a habit because you're scared of commits, you're doing it wrong. Commit early, commit often. Only stash when you cannot commit — dirty debug code, half-failed experiments, or when you absolutely must switch branches without a second commit.
// io.thecodeforge — devops tutorial // Scenario: You're refactoring payment-api and need to fix P1 bug # Wrong way — permanent loss git reset --hard # All refactoring work gone, customer data models deleted # Right way — safe shelving git stash push -m "payment-api refactor WIP" # Saved as stash@{0}: On payment-api-wip: payment-api refactor WIP git checkout main git pull origin main # Fix the emergency bug, commit, push git checkout payment-api-wip git stash pop # Back to work, zero lost changes
Where Does Git Stash Actually Live? (.git/refs/stash Exposed)
Every stash is a commit object. Not a temp file. Not a config entry. A real, bonafide Git commit with a parent tree.
Look inside .git/refs/stash and you'll find nothing but a SHA. Run 'git log --oneline stash@{0}' and you'll see a commit history of your stashes. That's because stash creates a merge commit with three parents: the original HEAD, the index state, and the working tree state. This is why you can branch off a stash, diff it, or even merge it.
Multiple stashes stack on .git/refs/stash as a reflog. 'git stash list' shows you the stack. 'git stash drop' removes the top commit from that reflog. 'git stash clear' nukes the entire reflog.
There's no magic repository in the sky. It's all refs and commits in your local .git directory. That also means stashing is purely local — no remote, no backup, no safety net. If your laptop catches fire, so do all your stashes.
Knowing this changes how you use stash. You can cherry-pick from a stash. You can merge stash changes. You can even push a branch from a stash when you realize it's a feature, not a quick fix.
// io.thecodeforge — devops tutorial // Visualize stash as real commits # See stash commits with full history git log --oneline --graph stash@{0} # Output: # * 3a4b5c6 WIP on payment-api-wip # |\ # | * 2b3c4d5 index on payment-api-wip: checkpoint # |/ # * 1a2b3c4 checkpoint before refactoring # Diff a specific file from stash git diff stash@{0} -- src/payment/api/validators.go # Create a branch from a stash (saves it permanently) git stash branch production-hotfix/payment-fix stash@{2}
Git Stash Best Practices
Stashing is a safety net, not a workflow crutch. The primary reason to stash is to temporarily set aside uncommitted changes when you need to switch branches, pull updates, or fix an urgent bug. Never stash as a substitute for meaningful commits. Before stashing, commit your work if it's logically complete—stashes are ephemeral and easy to lose. Use descriptive stash messages: git stash push -m "WIP: refactor auth module" saves hours of confusion later. Avoid piling up multiple stashes; each stash is a hidden breadcrumb that accumulates technical debt. Instead, pop and integrate each stash immediately after the interruption passes. Untracked files are not stashed by default unless you add -u or --include-untracked. This hidden trap causes accidental data loss when switching branches expecting all changes to travel with you. Finally, never keep a stash older than a few days—it signals that either the work should have been committed or it's dead code waiting to bite.
// io.thecodeforge — devops tutorial stash_rules: messages: required max_age_hours: 48 pop_before_new_work: true untracked_flag: -u commands: save_work: cmd: git stash push -m "refactor: extract billing service" restore: cmd: git stash pop check_stash_list: cmd: git stash list
Git Fundamentals
Git's core architecture is a directed acyclic graph of snapshots, not diffs. Every commit is a full snapshot of your tracked files at that moment—this is why operations like checkout and reset are fast. The three main areas working directory, staging index, and commit history define your workflow. Changes move from working tree to staging with git add, then to a commit with git commit. Git stash is a fourth temporary area: it saves the dirty state of your working directory and index as two separate commits stored under .git/refs/stash. Understanding this triad prevents confusion: git reset --soft only moves HEAD, --mixed resets the index, and --hard wipes working directory changes. Branches are lightweight pointers to commits; merging creates a new commit with two parents, rebasing rewrites history by replaying commits. Git's distributed nature means every clone has the full history. This makes local operations instant but adds complexity with remotes. Use git fetch before merging to inspect upstream changes without danger. The golden rule: never rewrite public history with rebase or reset --hard.
// io.thecodeforge — devops tutorial
areas:
working_directory:
state: dirty or clean
staging_index:
command: git add
commit_history:
command: git commit
stash_structure:
path: .git/refs/stash
type: two parent commits
key_rules:
fetch_before_merge: true
no_rewrite_public: trueStash Pop on Wrong Branch: Untracked Files Committed to Main
git stash to save their work — but stash only saves tracked files by default.
3. PaymentRetryService.java was untracked, so it was NOT stashed. It remained in the working directory.
4. Developer switched to main: git checkout main.
5. PaymentRetryService.java (the untracked file) followed them to main.
6. During the hotfix, they ran git add . which staged everything — including PaymentRetryService.java.
7. The file was committed to main with incomplete, non-compiling feature code.
8. CI built from main and failed on the compilation error.git revert <hotfix-commit-hash>.
2. The developer switched back to feature/payment-retry and stashed correctly: git stash push -u -m 'WIP: PaymentRetryService'.
3. Team rule: always use git stash push -u when stashing with untracked files.
4. Team rule: never run git add . on main — always add files individually or use git add -p.
5. Added pre-commit hook to warn when new files are staged on main that do not exist on the previous commit.- git stash does not include untracked files by default. Use git stash -u to include them.
- Untracked files follow you across branch switches. They are not part of the stash or the commit — they are just files in the working directory.
- Never run git add . on main. Stage files individually or use git add -p to review each hunk.
- Always label stashes with git stash push -m 'description'. Unnamed stashes become mystery boxes.
git reflog — find the stash application entry.
3. The stash commit object still exists: git fsck --no-reflogs | grep dangling — find dangling commit objects.
4. Recover: git stash apply <hash> using a hash that looks like your stash.
5. Prevention: use git stash apply when conflicts are possible. Then git stash drop manually after resolving.git reset --hard HEAD to restore the branch to its pre-pop state.
3. Switch to the correct branch: git checkout <correct-branch>.
4. Apply there: git stash apply stash@{0} (the stash is still in the stack if you used apply, or recover via reflog if you used pop).git stash list — read the messages (this is why you always use -m).
2. If messages are unhelpful: git stash show -p stash@{0} — full diff of each stash.
3. Search stashes for a specific change: git stash show -p stash@{N} | grep 'search-term' for each N.
4. Or: git log --all --oneline --grep='search-term' — stash entries appear in the log.git log --oneline -3 — find the commit that includes the wrong files.
3. Revert: git revert <commit-hash> to undo the commit on the wrong branch.
4. Switch to correct branch: git checkout <correct-branch>.
5. Stash correctly: git stash push -u -m 'WIP: new files' to include untracked files.git fsck --no-reflogs | grep dangling — find dangling commit objects.
3. Each stash entry is a commit internally. Filter by date: look for commits created around the time you stashed.
4. Apply: git stash apply <hash> for each recovered stash commit.
5. Prevention: never run git stash clear without checking git stash list first.git reflog | grep stash (find the stash application entry)git fsck --no-reflogs | grep dangling (find dangling commit objects)git reset --hard HEAD (undo the stash application)git checkout <correct-branch> (switch to where you need the stash)git stash list (read messages — this is why you always use -m)git stash show -p stash@{N} | grep 'search-term' (search each stash diff)git log --oneline -3 (find the commit with the wrong files)git revert <commit-hash> (undo the commit on the wrong branch)git fsck --no-reflogs | grep dangling (find lost stash commits)git stash apply <hash> (recover each stash commit)| Command | Effect | Stash Removed? | Best For |
|---|---|---|---|
| git stash | Stash tracked files | N/A | Quick context switch |
| git stash -u | Stash tracked + untracked | N/A | When you have new files |
| git stash pop | Apply + remove from stack | Yes | Normal restore — 95% of cases |
| git stash apply | Apply, keep in stack | No | Risky merges or multi-branch apply |
| git stash drop | Remove without applying | Yes | Discard stash you no longer need |
| git stash clear | Remove ALL stashes | Yes (all) | Spring cleaning — careful |
| git stash branch <name> | Create branch from stash | Yes | Long-lived stashes that deserve a branch |
| File | Command / Code | Purpose |
|---|---|---|
| 01_stash_basics.sh | git init && echo 'Initial' > tracked.txt && git add . && git commit -m 'Initial' | What Is Git Stash |
| io | git stash | Basic Stash |
| io | git stash apply stash@{0} | Stash Pop vs Apply |
| io | git stash push -m "WIP: retry config" -- src/main/java/io/thecodeforge/payment/R... | Stashing Partial Changes and Specific Files |
| ResetVsStash.yml | git reset --hard | Git Stash vs Git Reset |
| StashInternals.yml | git log --oneline --graph stash@{0} | Where Does Git Stash Actually Live? (.git/refs/stash Exposed |
| stash-best-practices.yml | stash_rules: | Git Stash Best Practices |
| git-fundamentals.yml | areas: | Git Fundamentals |
Key takeaways
Interview Questions on This Topic
Frequently Asked Questions
git stash pop applies the stash and immediately removes it from the stash stack. git stash apply applies it but leaves it in the stack. Use apply when there's a chance of merge conflicts (so the stash survives if the apply fails) or when you want to apply the same stash to multiple branches.
No, by default git stash only saves tracked files that have been modified. To include untracked files (new files not yet added with git add), use git stash push -u or git stash push --include-untracked.
Run git stash show stash@{N} for a summary of changed files, or git stash show -p stash@{N} for the full diff. Replace N with the stash index from git stash list.
Sometimes. Run git fsck --no-reflogs | grep commit to find dangling commit objects (stash entries are commits internally). You can then run git stash apply <hash> on any hash that looks like your lost stash. This works within the gc.reflogExpire window (default 30 days) as long as you haven't run git gc.
git stash pop deletes the stash from the stack even on conflict. The stash commit still exists in the object store. Run git fsck --no-reflogs | grep dangling to find dangling commit objects. Apply with git stash apply <hash>. Prevention: use git stash apply instead of pop when conflicts are possible.
Stashes are garbage-collected after gc.reflogExpire (default 90 days). Once collected, they are gone permanently. If you need changes longer than a few days, convert the stash to a branch with git stash branch <name> <stash>.
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
That's Git. Mark it forged?
6 min read · try the examples if you haven't