Skip to content

bunta-expert/git-path-audit

Repository files navigation

git-path-audit

Find Git paths that collide, change meaning, or fail to check out across Windows, macOS, and Linux.

English · 한국어 · 简体中文 · 繁體中文 · 日本語

CI Go Reference License: MIT

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.

Why this exists

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.

What it checks

Area Examples Rules
Raw path validity Invalid UTF-8, empty or traversal-like components GPA001GPA002
Unicode normalization Non-NFC names, NFC/NFD collisions GPA101GPA102
Case portability ASCII case collisions, heuristic Unicode full-case-fold collisions GPA103GPA104
Review safety Bidirectional controls and selected invisible format characters GPA105GPA106
Windows compatibility Forbidden characters, controls, device names, trailing dots/spaces, bounded canonical collisions GPA201GPA205

The exact conditions, severities, and known trade-offs are documented in the rule catalog.

Install

Requirements

  • 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.1

In 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 scan

Build from source

To 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 scan

On Windows, build with the same go build package argument and run the resulting git-path-audit.exe executable.

Usage

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

Synthetic and non-checkoutable paths

--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 --stdin0

The final non-empty record may omit its trailing NUL.

Source modes

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

Repository-wide scope

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/widget

A 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.

Output formats

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.sarif

Machine 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 codes

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 --strict

GitHub Actions

A 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 --strict

For code-scanning ingestion, produce SARIF and upload it with GitHub's upload-sarif action under the permissions appropriate for your repository.

Safety and determinism

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.1 behavior 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.

Scope and limitations

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.protectNTFS equivalence;
  • 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.

Documentation

Contributing

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.

License

MIT © 2026 bunta-expert

About

Read-only CLI that finds Git paths that collide, change meaning, or fail to check out across Windows, macOS, and Linux.

Topics

Resources

License

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages