Git stash records selected uncommitted work so a checkout can temporarily return to an earlier committed state. It is useful for a short interruption, but neither saving nor restoring is an unconditional promise about every file in the directory. The important questions are what was included, which base the work came from, what state the destination now has, and when the saved entry can be removed.
Know what the snapshot includes
An ordinary git stash push records tracked working-tree changes and the index, then rolls those changes back toward HEAD. Untracked files remain unless included with -u. Ignored files still remain with that option; -a includes ignored files too. Review the scope before selecting it, because ignored paths can contain credentials, large build outputs, or local data that should not enter Git objects.
Options for selected paths, staged changes, or keeping the index produce different results. Do not combine them by habit and assume they all describe the same snapshot. Inspect status and the intended diff first, give the entry a meaningful message, and inspect the saved contents before treating the original edits as recoverable.
A stash is represented by Git commit objects, not a special file outside the object database. The newest entry is referenced by refs/stash; older entries are retained through its reflog. They are not necessarily shown by ordinary branch history. A forgotten entry is not automatically lost, but deleting its retention references can eventually make recovery impossible. A local branch or stash also does not by itself provide an independent off-machine backup.
Applying is different from popping
git stash apply attempts restoration and retains the saved entry. This is useful when the restored result must be reviewed before removing the saved copy. By default, do not expect the original staged versus unstaged distinction to be reinstated. --index additionally attempts to restore index state, which can fail if conflicts or destination changes prevent it.
git stash pop applies an entry and removes it from the list only when application succeeds. A conflicting pop retains the entry, but may already have changed cleanly applicable paths. A nonzero status therefore does not mean the entire operation left the working tree untouched. Inspect the index and files before trying another application.
Entries are not private to a branch or linked worktree. Their descriptive branch name is context, not a rule forcing restoration there. Numbered selectors can change as entries are added or dropped, so inspect and record the exact object before a sensitive operation. Quote selectors such as 'stash@{0}' when passing them through a shell.
When it breaks
Restoration onto a changed base can conflict or produce behavior that needs adaptation even without text conflicts. Resolve using the intended final code, stage the specific paths, and test the result. Do not repeatedly pop the same retained entry into a partially restored tree. Stash application has no general abort command that automatically reverses every partial effect.
When the original base is the right place to resume, git stash branch creates a branch there and applies the saved work. A successful application also drops an entry specified through the stash reflog. It is a recovery workflow tied to the recorded base, not a solution requiring two stashes or a particular elapsed time.
Removing an entry should follow verification, not substitute for it. Broad stash clear can discard unrelated saved work, and dropped objects may later be pruned. For work that needs ongoing review, make an intentional commit on a suitable branch and preserve it according to the project’s backup and sharing policy. Stash is not a promise to capture editor buffers, arbitrary filesystem metadata, or every external resource a build uses.
Check it quickly
Run this with Python 3 and Git 2.28 or newer. All changes and removals happen in a disposable repository with example identity, disabled hooks and signing, excluded personal Git configuration and routing variables, and no remote. The fixture deliberately gives the index and working file different values.
import os
import subprocess
import tempfile
from pathlib import Path
env = {key: value for key, value in os.environ.items() if not key.startswith("GIT_")}
env.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=os.devnull)
with tempfile.TemporaryDirectory(prefix="git-stash-demo-") as directory:
root = Path(directory)
def git(*args, check=True):
return subprocess.run(
["git", "-c", "user.name=Stash Example",
"-c", "user.email=stash@example.invalid",
"-c", "commit.gpgsign=false", "-c", "core.hooksPath=/dev/null", *args],
cwd=root, env=env, text=True, capture_output=True, check=check,
)
git("init", "-q", "-b", "main")
(root / ".gitignore").write_text("ignored.txt\n")
(root / "note.txt").write_text("base\n")
git("add", ".gitignore", "note.txt")
git("commit", "-qm", "base")
(root / "note.txt").write_text("staged\n")
git("add", "note.txt")
(root / "note.txt").write_text("working\n")
(root / "new.txt").write_text("untracked\n")
(root / "ignored.txt").write_text("ignored\n")
git("stash", "push", "-qm", "tracked only")
saved = git("rev-parse", "refs/stash").stdout.strip()
print("default-leaves-untracked:", (root / "new.txt").exists()
and (root / "ignored.txt").exists()
and (root / "note.txt").read_text() == "base\n")
git("stash", "apply", "--index", saved)
restored = (git("show", ":note.txt").stdout == "staged\n"
and (root / "note.txt").read_text() == "working\n")
assert restored
print("index-and-work-restored:", restored)
print("apply-keeps-entry:", git("rev-parse", "stash@{0}").stdout.strip() == saved)
assert git("rev-parse", "stash@{0}").stdout.strip() == saved
git("stash", "drop", "-q", "stash@{0}")
git("stash", "push", "-qu", "-m", "tracked and untracked")
saved = git("rev-parse", "stash@{0}").stdout.strip()
print("include-untracked-not-ignored:", not (root / "new.txt").exists()
and (root / "ignored.txt").exists())
(root / "note.txt").write_text("upstream\n")
git("add", "note.txt")
git("commit", "-qm", "new base")
conflict = git("stash", "pop", "stash@{0}", check=False)
print("conflicting-pop-keeps-entry:", conflict.returncode != 0
and bool(git("ls-files", "-u").stdout)
and git("rev-parse", "stash@{0}").stdout.strip() == saved)
(root / "note.txt").write_text("upstream\nworking\n")
git("add", "note.txt", "new.txt")
assert not git("ls-files", "-u").stdout
assert git("show", ":note.txt").stdout == "upstream\nworking\n"
assert (root / "new.txt").read_text() == "untracked\n"
git("commit", "-qm", "retain both fixture values")
assert git("rev-parse", "stash@{0}").stdout.strip() == saved
git("stash", "drop", "-q", "stash@{0}")
print("resolved-and-verified:", not git("status", "--porcelain").stdout
and not git("stash", "list").stdout)
All six checks should print True. The default leaves untracked and ignored files, while indexed application restores the two tracked states and retains the entry. Including untracked files still excludes the ignored file. A later conflicting pop retains its entry; the fixture explicitly resolves to both intended values, commits and verifies that result, then drops only the identified entry. Python finally removes all disposable data, including the ignored file.
Sources
- Git stash reference, snapshot scope, stored entries, restoration, and removal semantics, checked 2026-09-07.
- Pro Git: stashing and cleaning, index restoration and resuming work from its original base, checked 2026-09-07.