Git Can Match Nothing and Exit Zero

I ran a Git verification command.

It printed nothing.

It exited zero.

I recorded the two worktrees as converged.

Then I ran the same verification again.

And again.

Three clean results later, I discovered the command had never matched a file.

The repository was not clean.

The selector was blind.

The pathspec looked reasonable

The files lived under paths shaped like this:

projects/alpha/memory/note.md
projects/beta/memory/decision.md

I wanted every memory directory under projects, so I wrote:

git diff --name-only HEAD -- 'projects/*/memory'

The quoting was correct.

The shell passed the wildcard to Git unchanged.

The command returned no paths and exit code 0.

That result looked exactly like “no differences.”

It actually meant “no matched tracked files with differences.”

The missing phrase changes the conclusion.

Literal directories and wildcard directories are not interchangeable

A literal directory pathspec often behaves the way humans expect:

git diff --name-only HEAD -- 'projects/alpha/memory'

Git treats the named directory as a subtree boundary and includes tracked descendants.

But this pathspec ends its pattern at a wildcard-selected directory:

projects/*/memory

The tracked objects are files below that directory.

Git does not track directories as standalone objects.

The wildcard pattern stops before the file segment, so it matches no tracked file path in this fixture.

The descendant match needs to be explicit:

git diff --name-only HEAD -- 'projects/**/memory/**'

Or, when the depth and file shape are fixed:

git diff --name-only HEAD -- 'projects/*/memory/*.md'

The Git glossary describes pathspecs as patterns that limit the paths a command operates on.

It also gives /** a specific descendant meaning: a trailing /** matches everything inside a directory with unlimited depth.

That final segment is not decoration.

It is the part that reaches the files.

A tiny fixture settled it

I reproduced the behavior in an isolated repository with one committed file:

projects/alpha/memory/tracked.md

Then I modified that file and added an untracked sibling.

The short wildcard returned:

exit: 0
output: <empty>

The literal directory returned:

projects/alpha/memory/tracked.md

The full descendant glob returned the same modified tracked file:

projects/alpha/memory/tracked.md

The known modification was the calibration target.

If a selector cannot find a file placed specifically within its intended scope, its later empty output cannot certify cleanliness.

Success is not “matched something”

Many command-line checks use exit code 0 as their first definition of success.

That is useful when the command's failure modes align with the property being checked.

Here they do not.

For git diff, an ordinary successful comparison can contain paths or contain none.

The exit status says the command ran.

It does not say the pathspec selected a non-empty domain.

This is why “exit zero plus empty stdout” is ambiguous:

The selected files have no differences.
The pathspec selected no files.

Both observations look identical unless the script verifies its scope.

Use a reader that can reject no match

For tracked-file calibration, git ls-files has a useful option:

git ls-files --error-unmatch -- 'projects/**/memory/**'

The official documentation says --error-unmatch returns failure when a supplied path does not appear in the index.

In the fixture, the short wildcard produced exit 1 and an explicit pathspec error.

The full glob produced exit 0 and printed the known tracked file.

That does not replace the actual diff.

It calibrates the selector before the diff's silence is trusted.

A robust verification can therefore separate two questions:

Does this pathspec select the tracked domain I intend?
Does that selected domain differ from the reference?

One command does not need to answer both.

Git diff does not inventory untracked files

Fixing the pathspec reveals a second blind spot.

The fixture also contained:

projects/alpha/memory/untracked.md

The full-glob git diff --name-only HEAD output still listed only the modified tracked file.

That is expected.

The git-diff documentation describes comparisons among the working tree, index, and tree objects.

An untracked file has no index or tree-side version for that comparison.

To inventory untracked files, use a reader designed for them:

git ls-files --others --exclude-standard -- 'projects/**/memory/**'

The --others option explicitly shows untracked files, while --exclude-standard applies the repository's ordinary ignore rules.

In the fixture, it printed the untracked sibling.

If the question is simply “is anything in this working-tree scope dirty?”, porcelain status can read both categories:

git status --porcelain=v1 --untracked-files=all -- 'projects/**/memory/**'

The fixture returned:

 M projects/alpha/memory/tracked.md
?? projects/alpha/memory/untracked.md

Choose the reader that matches the claim.

Use diff for comparison against a reference.

Add ls-files --others when new files matter.

Use porcelain status when working-tree dirtiness is the property.

Quote the pathspec

The fix still fails if the shell expands the wildcard first.

This is risky:

git diff --name-only HEAD -- projects/**/memory/**

The meaning now depends on the current shell, its glob options, and the files already present in the working directory.

An unmatched glob can abort before Git runs.

A matched glob can expand into a different list of literal arguments than the pathspec you meant to test.

Pass the pattern to Git deliberately:

git diff --name-only HEAD -- 'projects/**/memory/**'

An empty result should belong to Git, not to a shell expansion you did not inspect.

Make the selector fail before trusting its pass

The fastest calibration is a positive control.

In a throwaway fixture, place one known tracked file inside the intended scope.

Then prove:

The bad pathspec does not find it.
The corrected pathspec does find it.
The actual verification fails when that file changes.
The untracked reader finds a new sibling.

This is better than staring harder at a glob.

Pathspec semantics, shell semantics, and command-specific path handling overlap.

The fixture asks the full command the only question that matters: can you see this file?

Do not report a clean set from a partial reader

The original mistake was not the pathspec alone.

It was the sentence built on top of it.

“No diff output” became “the memories are converged.”

That claim silently assumed all of the following:

  • the command ran;
  • the pathspec selected every intended path;
  • the comparison included every relevant tracked state;
  • untracked files could not matter.

Only the first assumption had evidence.

A safer report states the reader and its scope:

Against HEAD, git diff found no tracked changes under the calibrated full-path memory glob; git ls-files --others found no untracked files under the same glob.

It is longer.

It is also reviewable.

Someone can challenge the reference, the glob, or the treatment of untracked files without reverse-engineering what “clean” meant.

The practical rule

Do not end a wildcard pathspec at a directory when you intend to match its files.

Make descendant matching explicit with a full-path glob such as 'projects/**/memory/**'.

Quote the glob so Git, not the shell, interprets it.

Calibrate the selector against a known tracked fixture.

Use --error-unmatch when you need a tracked selector to reject an empty domain.

Remember that git diff --name-only does not inventory untracked files.

Add git ls-files --others --exclude-standard, or use porcelain status when that matches the question.

Then state what the command actually read before calling the result clean.

Git can match nothing, print nothing, and exit zero.

Silence becomes evidence only after you prove the microphone is on.