I moved a directory this week and quietly un-ignored a file containing live
credentials. Nothing failed. Nothing warned. git status simply began offering
to commit a file it had been hiding for months, and if I had run git add -A
without looking, the Supabase URL and key would have gone to a remote.
The cause is one sentence of the gitignore spec that I knew, had never had to
think about, and got caught by anyway.
The move
The repository holds a Swift app. An Android app arrived, so the top level got
lopsided — one platform in a folder, the other spread across App/, Checks/,
Support/ and a couple of scripts at the root. I moved the Swift half under
apple/, so apple/ and android/ sat side by side.
Pure git mv. No file contents changed. All the checks passed, both platforms
built, and it looked like exactly the boring structural commit it was.
Then, out of habit, I looked at what git was proposing to commit:
?? apple/Support/pooled-flight/
?? apple/Support/telemetry.env
Both of those had been ignored an hour earlier.
The rule
Here is the relevant part of the .gitignore:
build/
*.xcodeproj
dist/
Support/telemetry.env
Support/pooled-flight/
Five patterns. Three of them survived the move and two did not, and the
difference is a slash:
If a pattern contains a slash anywhere except at the end, it is treated as
relative to the directory containing the.gitignorefile. Otherwise it
matches at any depth.
build/ has a slash only at the end, so it is a name — it matches build/ at
any depth, including apple/build/. *.xcodeproj has no slash at all, so it
matches anywhere too.
Support/telemetry.env has a slash in the middle. That makes it anchored to
the repository root. It means <root>/Support/telemetry.env and nothing else.
The moment the file became apple/Support/telemetry.env, the pattern stopped
describing it, and the file became an ordinary untracked file that git was
helpfully offering to add.
This is correct behaviour. It is documented. It is also completely silent: there
is no warning when a pattern matches nothing, because a pattern matching nothing
is a normal state for most patterns most of the time.
Neither file moved relative to the other, and the .gitignore was not edited. The tree grew a directory above them both, which is all it takes.
Checking, rather than assuming
git check-ignore -v answers the question directly, and it names the pattern and
line number that did the ignoring:
$ git check-ignore -v apple/Support/telemetry.env
# ← no output. Not ignored.
$ git check-ignore -v apple/build
.gitignore:1:build/ apple/build # ← still fine
Empty output means not ignored. That asymmetry — silence for the dangerous
answer — is worth knowing before you rely on it.
The other question worth asking immediately, and the one I was most anxious
about, is whether the file had ever been committed on any branch at any point:
$ git log --all --oneline -- '*telemetry.env'
# ← empty. Never committed.
It had not. A near miss rather than an incident. If it had not been empty, the
fix is very different and much worse: rotate the credential first, then worry
about rewriting history, in that order and never the other way round.
The fix, and why it is the fix
The obvious repair is to re-point the patterns:
apple/Support/telemetry.env
apple/Support/pooled-flight/
That works today and breaks the next time anything moves. It is the same bug,
rearmed.
The better repair is to stop anchoring them at all:
telemetry.env
pooled-flight/
No slash in the middle, so they match by name at any depth, wherever the tree
goes next. Slightly broader than strictly necessary — any file called
telemetry.env anywhere is now ignored — which for a secret is the correct
direction to be wrong in.
What I would take from this
A gitignore pattern with a slash in it is coupled to your directory
structure. For build output that is fine; if dist/ moves, you find out
because your build breaks. For secrets it is a silent coupling to something you
will eventually change for unrelated reasons, and the failure mode is that the
secret becomes committable at the exact moment you are busy thinking about
something else.
Prefer name-only patterns for anything sensitive. telemetry.env rather than
config/telemetry.env. *.p12 rather than keys/*.p12. You lose a little
precision and gain immunity to every future refactor.
Look at git status after a structural change, specifically for new untracked
files. Not for the files you moved — git shows those as renames and they are
fine. For the ones that appear from nowhere. Anything newly untracked after a
move was previously ignored by an anchored pattern, and that is the entire class
of this bug.
And run git check-ignore -v on your secrets after moving anything. It takes
one command and it is the only way to get a straight answer, because the
dangerous result is the one that prints nothing at all.
ExtendPilot shares a Mac's screen with the iPhones and iPads already in the
room — as a mirror, or as a second desktop macOS treats as real hardware. Peer
to peer over your own Wi-Fi, no account, no server in a session. It is not out
yet. This one came out of restructuring the repo to hold an Android app
alongside the Swift one.

Top comments (0)