A missing file is not always a Git deletion. An untracked file can disappear without affecting the index, while a file removed from the index is no longer part of the same unstaged-deletion query. For scripts that specifically need paths still present in the index but missing from the working tree, use a narrowly defined Git query.

Prerequisites: Git and Python 3.12 on Linux, with permission to create a temporary directory under the current scratch directory. The example was tested with Git 2.43.0. It creates a disposable repository, makes no commits, configures no identity and uses no remote.

Query the index-to-working-tree relationship

The official git ls-files manual defines --deleted as selecting unstaged deletions. Adding -z terminates records with NUL and avoids filename quoting. This matters because a legal Linux filename can contain a newline.

Save the following as deleted.py. All mutations occur inside the temporary demonstration repository.

from pathlib import Path
import os
import subprocess
import tempfile

# Create only a disposable repository under the current scratch directory.
with tempfile.TemporaryDirectory(prefix="git-demo-", dir=".") as directory:
    root = Path(directory).resolve()
    env = os.environ.copy()
    env.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=os.devnull,
               GIT_CONFIG_COUNT="0", GIT_LITERAL_PATHSPECS="1")
    def git(*args):
        return subprocess.run(["git", "-C", str(root), *args], env=env,
                              check=True, stdout=subprocess.PIPE,
                              stderr=subprocess.PIPE, timeout=5).stdout
    git("init", "-q")
    name = "missing\nreport.txt"
    (root / name).write_text("demo", encoding="utf-8")
    (root / "kept.txt").write_text("kept", encoding="utf-8")
    git("add", "--", name, "kept.txt")
    (root / name).unlink()
    (root / "untracked.txt").write_text("ignored by query", encoding="utf-8")
    raw = git("ls-files", "--deleted", "-z", "--")
    missing = raw.split(b"\0")[:-1]
    assert missing == [name.encode()]
    print("missing:", repr(os.fsdecode(missing[0])))
    (root / name).write_text("restored", encoding="utf-8")
    assert git("ls-files", "--deleted", "-z", "--") == b""
    git("add", "--", name)
    git("rm", "--cached", "--", name)
    (root / name).unlink()
    assert git("ls-files", "--deleted", "-z", "--") == b""
    print("restored and removed-from-index checks passed")

Run and compare

python3 deleted.py

Expected output:

missing: 'missing\nreport.txt'
restored and removed-from-index checks passed

The displayed backslash-n is Python's escaped representation of the newline inside one filename. It does not represent two missing files. The raw assertion verifies that precisely one NUL-delimited path was returned. Keeping output as bytes until record separation also avoids premature filename-decoding assumptions.

Understand the transitions

After restoring the file, the unstaged-deletion list is empty even though its contents differ from the indexed version. The example then stages the restored contents before removing that entry from the index. This setup avoids asking Git to discard an index version that differs from the working file. Once the index entry is gone, deleting the remaining working file does not make it appear in this query.

Edge cases and troubleshooting

This command is not a complete clean-worktree check and does not report all staged deletions, modifications or untracked files. Use the appropriate status or diff interface for those requirements. A repository error raises an exception through check=True; it must not be interpreted as an empty result.

Do not split the output with line-oriented utilities or store NUL-delimited data in a shell variable. The demo assumes ordinary indexed files in a repository without merge conflicts, sparse-checkout rules or submodules; those cases were not exercised. The temporary-directory context removes only the repository it created.

Continue with Git tutorials and Linux tutorials.