Find Git paths that collide, change meaning, or fail to check out across Windows, macOS, and Linux.
English · 한국어 · 简体中文 · 繁體中文 · 日本語
git-path-audit is a read-only command-line auditor for path names stored in Git. It catches cross-platform checkout hazards, Unicode ambiguity, Windows-incompatible names, and invisible formatting characters before they surprise another contributor—or mislead a reviewer.
It uses Git metadata as its source of truth. It does not walk the working tree, read file contents, rename files, or modify the repository.
Git can store path names that the current filesystem cannot safely represent. A repository may work on one machine and fail—or silently collide—on another.
Common examples include:
# Same letters, different Unicode normalization
docs/한글.md
docs/한글.md
# Distinct on a case-sensitive volume, conflicting elsewhere
src/Foo.ts
src/foo.ts
# Reserved or rewritten by Windows
assets/CON.txt
docs/readme.
# Visually deceptive
review/ab\u{202E}cd.txt
A scan reports the problem with a stable rule ID and a safely escaped path:
ERROR GPA102 unicode-normalization-collision: "docs/한글.md" — paths collide after Unicode NFC and NFD normalization related=["docs/한글.md"]
ERROR GPA103 ascii-case-collision: "src/Foo.ts" — paths collide after ASCII-only case folding related=["src/foo.ts"]
ERROR GPA203 windows-reserved-device-name: "assets/CON.txt" — path contains 1 Windows reserved device-name occurrence(s)
WARNING GPA105 bidi-control: "review/ab\u{202E}cd.txt" — path contains U+202E RIGHT-TO-LEFT OVERRIDE
Scanned 6 unique path(s): 3 error(s), 1 warning(s), 0 note(s).
The example above is abridged; real diagnostics also include component details and a rule help URI.
| Area | Examples | Rules |
|---|---|---|
| Raw path validity | Invalid UTF-8, empty or traversal-like components | GPA001–GPA002 |
| Unicode normalization | Non-NFC names, NFC/NFD collisions | GPA101–GPA102 |
| Case portability | ASCII case collisions, heuristic Unicode full-case-fold collisions | GPA103–GPA104 |
| Review safety | Bidirectional controls and selected invisible format characters | GPA105–GPA106 |
| Windows compatibility | Forbidden characters, controls, device names, trailing dots/spaces, bounded canonical collisions | GPA201–GPA205 |
The exact conditions, severities, and known trade-offs are documented in the rule catalog.
- Go 1.25 or newer to install or build
- Git 2.45 or newer for index, staged, and tree scans
--stdin0 does not invoke Git and can be used without Git installed.
Install the tagged release:
go install github.com/bunta-expert/git-path-audit/cmd/git-path-audit@v0.0.1In a POSIX shell, ensure the Go binary directory is on PATH:
export PATH="$(go env GOPATH)/bin:$PATH"Because the executable is named git-path-audit, Git can discover it as a subcommand:
git path-audit scanTo build from source in a POSIX shell:
git clone https://github.com/bunta-expert/git-path-audit.git
cd git-path-audit
go build -o git-path-audit ./cmd/git-path-audit
./git-path-audit scanOn Windows, build with the same go build package argument and run the resulting git-path-audit.exe executable.
The default source is the current repository index.
# Audit every tracked path in the index
git-path-audit scan
# The `scan` subcommand is optional
git-path-audit .
# Audit staged added, copied, modified, and rename-destination paths
git-path-audit scan --staged
# Audit a named tree or revision without trusting the working-tree layout
git-path-audit scan --tree HEAD
# Audit another repository
git-path-audit scan ../another-repository
# A descendant still selects that repository's complete index
git-path-audit scan ../another-repository/packages/widget--stdin0 accepts NUL-delimited raw path records. It is useful for tests and for names the host filesystem cannot materialize. In a POSIX shell:
printf 'src/Foo.ts\0src/foo.ts\0' | git-path-audit scan --stdin0The final non-empty record may omit its trailing NUL.
Exactly one source mode may be active.
| Mode | Invocation | Input |
|---|---|---|
| Index | git-path-audit scan |
Every tracked path in the selected repository's complete index |
| Staged | git-path-audit scan --staged |
Repository-wide staged A/C/M/R destination paths; deleted paths are excluded |
| Tree | git-path-audit scan --tree <revision> |
Every recursive path from a verified Git tree-ish |
| stdin0 | git-path-audit scan --stdin0 |
Arbitrary NUL-delimited raw path bytes from standard input |
For every Git-backed mode, the repository argument may be the selected worktree's root or any descendant that Git can discover on the same filesystem. Both forms scan the complete selected repository—not just the named directory or the current directory—and every raw path is relative to the repository root. For example, these commands select the same index and produce the same report:
git-path-audit scan /work/project
git-path-audit scan /work/project/packages/widgetA root-level finding is therefore still reported from packages/widget, and a
path there is emitted as packages/widget/file.go, never as file.go or with
../ components. Staged scans enforce this contract even when repository or
user configuration sets diff.relative; that setting cannot exclude paths
outside the descendant or rewrite their names. Tree scans likewise enumerate
the complete selected tree.
Version 0.0.1 has no subtree scan mode: passing a descendant selects a
repository, not a narrower scope. Git's default discovery does not cross a
filesystem mount boundary, so pass the worktree root when a descendant is on a
different filesystem. --stdin0 is unchanged: it accepts the supplied raw path
records directly, has no repository argument, and performs no Git discovery.
Git-backed scans invoke Git directly, disable repository fsmonitor hooks and optional locks, disable lazy object fetching, and use NUL-delimited plumbing output. The repository argument is authoritative: inherited Git variables cannot substitute another repository, index, object store, worktree, ref namespace, or discovery policy. Environment names are isolated case-insensitively, including duplicates, without changing the parent process. If a partial clone is missing an object required for a tree scan, the command fails offline rather than contacting a promisor remote.
Tree scans intentionally honor ordinary refs/replace/ replacements stored in
the selected repository, but ignore inherited replacement controls. Objects
available only through a receive-hook quarantine are outside the v0.0.1
Git-backed contract; run after object promotion or supply paths with --stdin0.
See ADR 0003
for the exact child-environment policy and
ADR 0004
for the repository-scope plumbing decision.
Text is the default. JSON and SARIF 2.1.0 are deterministic and suitable for automation.
git-path-audit scan --format json > git-path-audit.json
git-path-audit scan --tree HEAD --format sarif > git-path-audit.sarifMachine reports preserve every path losslessly with raw_path_base64. Valid UTF-8 paths may also appear in a convenience path field, while display_path is always safely escaped for presentation. Reports contain no timestamps, local absolute repository paths, random identifiers, raw control bytes, or ANSI sequences.
| Exit | Meaning |
|---|---|
0 |
Scan completed with no error findings; warnings are allowed unless --strict is set; note-only reports remain successful |
1 |
At least one error finding, or at least one warning under --strict |
2 |
Invalid invocation, Git/source failure, malformed input, or report/write failure |
Use strict mode in CI when warnings should also block a change:
git-path-audit scan --strictA minimal check can install the pinned release and audit the checked-out index:
name: Path portability
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
audit-paths:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version: '1.25.x'
- name: Install git-path-audit
run: go install github.com/bunta-expert/git-path-audit/cmd/git-path-audit@v0.0.1
- name: Audit tracked paths
run: git-path-audit scan --strictFor code-scanning ingestion, produce SARIF and upload it with GitHub's upload-sarif action under the permissions appropriate for your repository.
Path names are treated as hostile byte input.
- Git is invoked directly, never through a shell.
- Repository files and blobs are not read as scan input.
- Audited paths are never renamed, normalized, deleted, or modified.
- Raw bytes are retained independently from Unicode interpretation.
- ANSI controls, newlines, invalid bytes, bidi controls, and selected invisible characters are escaped at presentation boundaries.
- JSON and SARIF preserve raw identity with base64 and deterministic ordering.
- Unicode normalization and full case-fold behavior are frozen to Unicode 15.0.0 for the
v0.0.1behavior contract. - Git cancellation and deadline failures produce operational exit
2, no report on stdout, and one bounded, safely encoded stderr line.
See SECURITY.md for the reporting process and security boundaries, and ARCHITECTURE.md for implementation details.
git-path-audit implements a conservative portability model. It is not an exact emulator for APFS, HFS+, NTFS, FAT, SMB, every locale, or every API layered on top of those filesystems.
Version 0.0.1 intentionally does not provide:
- exact NTFS 8.3 alias or Git
core.protectNTFSequivalence; - exact HFS protection behavior;
- path-length policies;
- fuzzy homoglyph or mixed-script detection;
- untracked-file or file-content scanning;
- baselines, suppressions, or repository configuration;
- automatic rename or fix operations;
- subtree-limited Git-backed scans (a descendant still selects the complete repository);
- receive-hook scans that depend on unpromoted quarantine objects;
- inherited Git ref namespaces or non-default repository discovery across a filesystem boundary (pass the repository root instead).
A clean result means no path violated this version's documented rules—not that every possible filesystem will accept every checkout.
- Product specification
- Rule catalog
- Architecture
- Unicode version decision
- JSON help URI decision
- Git environment isolation decision
- Repository-wide Git source decision
- Contributing
- Security policy
- Changelog
Bug reports and focused pull requests are welcome. Path handling is security-sensitive, so behavior changes should include regression tests using raw NUL-delimited fixtures when a filename cannot be represented safely on every host.
Read CONTRIBUTING.md before submitting a change. Please report exploitable path payloads through GitHub's private vulnerability reporting rather than a public issue.
MIT © 2026 bunta-expert