Unstage a file without discarding its edits
Before unstaging a tracked text file, save its staged changes with git diff --cached -- note.txt > staged.patch, choosing an unused patch filename: staged-only content can otherwise be lost. Then use git restore --staged -- note.txt to reset its index entry to the current commit while leaving the working file unchanged. Inspect both differences afterward; this does not preserve the old staging selection.
Separate the file from the version being committed
A file’s working contents and its staged contents are two different versions. Staging records the content available at that moment; another edit afterward does not silently update the staged copy1. This is why the same path can appear in both the staged and unstaged parts of status. Nothing is necessarily wrong: the two versions simply differ.
Before unstaging, identify the path and inspect its changes. The intention here is to keep editing the file while removing it from the next commit. It is not to restore an older file, erase an accidental change or undo a commit. Those tasks need different decisions, and similar command names make it easy to choose the wrong destination.
The --staged option is the important part of the command. With that option and no explicit source, restore takes the index’s version from the current commit and leaves the working file alone2. The separate -- marks the beginning of the path argument. Keep the path narrow when the goal concerns one file rather than the whole repository.
Run the complete example in an empty directory
The example creates one committed text file, stages a change, then edits that file again. It also stages a new file that has never appeared in a commit. This gives two distinct cases: unstaging a modified tracked file and unstaging a newly added file. Neither case needs a graphical client or a remote repository.
Download the complete example. Run it with Git 2.56.0 in an empty directory; it creates a new unstage-example directory and stops if that directory already exists.
#!/bin/sh
# SPDX-License-Identifier: MIT
set -eu
export TZ=UTC LC_ALL=C
export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null
export GIT_AUTHOR_NAME='Example Author'
export GIT_AUTHOR_EMAIL='author@example.invalid'
export GIT_COMMITTER_NAME='Example Author'
export GIT_COMMITTER_EMAIL='author@example.invalid'
export GIT_AUTHOR_DATE='2026-10-25T12:00:00+00:00'
export GIT_COMMITTER_DATE='2026-10-25T12:00:00+00:00'
test "$(git version)" = 'git version 2.56.0'
mkdir unstage-example
cd unstage-example
git init -q --template= --initial-branch=main --object-format=sha1
git config commit.gpgsign false
git config core.autocrlf false
printf 'base\n' > note.txt
git add note.txt
git commit -qm 'Base note'
head_before=$(git rev-parse HEAD)
printf 'base\nstaged line\n' > note.txt
git add note.txt
printf 'later edit\n' >> note.txt
printf 'new file\n' > draft.txt
git add draft.txt
file_before=$(git hash-object note.txt)
new_before=$(git hash-object draft.txt)
printf 'Before: one staged addition and one twice-edited file\n'
git status --short
git diff --cached -- note.txt
git diff -- note.txt
git diff --cached -- note.txt > staged.patch
test -s staged.patch
git restore --staged -- note.txt
test "$(git rev-parse HEAD)" = "$head_before"
test "$(git hash-object note.txt)" = "$file_before"
test "$(git show :note.txt)" = base
test "$(git status --porcelain=v1 -- note.txt)" = ' M note.txt'
printf 'Tracked file: all local lines remain; none are staged\n'
git diff --cached -- note.txt
git diff -- note.txt
git restore --staged -- draft.txt
test "$(git hash-object draft.txt)" = "$new_before"
test "$(git status --porcelain=v1 -- draft.txt)" = '?? draft.txt'
printf 'New file: still present, now untracked\n'
git status --short
git diff --cached --exit-code
git add note.txt
test "$(git show :note.txt)" = "$(cat note.txt)"
printf 'Restaging takes the current complete note.txt content\n'
git diff --cached -- note.txt
Read the state before unstaging
At the first status check, draft.txt is a staged addition. note.txt has both staged and unstaged modifications. Its staged version contains the original line and staged line; its working version also contains later edit. These labels describe the example’s content, not a property Git attaches to a particular line.
The script prints both differences for note.txt before changing anything and saves the staged text patch before restoring the index. The staged comparison contains the first addition; the working comparison contains the later addition relative to that staged copy. Keep the patch until its content is no longer needed: an index version can contain text absent from both the commit and the working file.
Do not replace that inspection with a quick glance at a filename. A file may contain a small fix alongside a temporary diagnostic, unfinished wording or an unrelated change. Knowing that the path is modified does not establish which parts belong together. Read enough context to recognize the intended change before adjusting its staged state.
Case one: unstage a tracked modification
The first restore command targets only note.txt. The script checks that the branch still points to the same commit and that the file’s content hash has not changed. It also reads the index’s version and verifies that it has returned to the committed base line. These checks distinguish “the command succeeded” from “the intended content stayed present.”
Afterward, the staged comparison for this path is empty. The working comparison includes both added lines because both now differ from the original committed version. Status shows a blank first column and M in the second. The file remains tracked; its edits simply are not selected for the next ordinary commit.
This result does not preserve the earlier partial staging selection. The working contents survive, but the old index version is replaced. If that version contains staged-only text, unstaging does not put that text back into the working file. Save and inspect it before the command; the example’s text patch preserves the earlier selected change.
Case two: unstage a newly added file
The second restore command targets draft.txt. Because this file was not in the current commit, it no longer has a staged entry afterward. The file still exists and its content hash matches the value recorded before the command. Status marks it with ??, indicating an untracked path in this repository state3.
Untracked is a useful warning to keep in view. The file is not part of the next commit merely because it remains beside the tracked files. Check the next commit’s intended contents before proceeding, especially when the new file is necessary for another staged change. The example does not delete it or treat its presence as proof that someone else has a copy.
Before the first commit, there is no HEAD snapshot and git restore --staged fails. For a newly staged file in that state, use git rm --cached -- draft.txt to remove its index entry while keeping the file4. If Git refuses because the index differs from the working copy, preserve and inspect both versions rather than forcing removal. On an already committed file, git rm --cached instead stages a deletion; it is not interchangeable with unstaging an edit.
Select the next change deliberately
At this point the script verifies that no differences remain staged. It then stages note.txt again and checks that the staged content matches the complete current file. This includes both added lines. That is the outcome requested by the example, not an automatic recreation of the earlier partial selection.
For a real change, decide whether the full current file is what the next commit should contain. If it is not, review and select smaller changes before committing. The staged and working differences guide walks through the comparisons. The local commit guide covers a different starting point: the change has already become a commit.
Finish by checking the whole staged set, not just the path you changed. Unstaging one file does not imply that every other staged file is appropriate. The example prints repository-wide status so that the untracked draft stays visible while the tracked note is prepared for a commit.
Does resolved-path staging replace this command?
No. git add --resolved is marker-based staging for unmerged paths, not an unstaging command5. Binary and deletion conflicts can lack markers without being resolved correctly. Resolve those yourself, stage only the explicit paths you intend, and check git diff --cached before committing; the option does not validate your resolution.
Not covered on this page
- Restoring or discarding working-file contents.
- Recovering an index version that was not saved before replacement.
- Resolving conflicts or choosing individual patch hunks.
- Undoing a commit, deleting a file or removing historical content.
Sources
How we test: methodology. Found a mistake? Corrections.