Skip to content

Latest commit

 

History

35 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Pathguard

🇺🇸 English · 🇰🇷 한국어

A read-only watcher that catches filenames which will silently break your cloud/NAS sync — before they do.

CI platform license

pathguard scans a synced folder (Synology Drive · Dropbox · Nextcloud · OneDrive · iCloud Drive, etc.) and flags files whose name or path byte length may exceed the destination's NAME_MAX / PATH_MAX once Unicode is decomposed to NFD — a sync failure waiting to happen. It only reads; it never touches your files.


Why it matters — the NFD byte blowup

Filesystem limits NAME_MAX 255 / PATH_MAX 4096 count UTF-8 bytes, not characters. And the byte length of the same text depends on its Unicode normalization form. Korean is the worst offender:

Form 보고서 ("report") UTF-8 bytes
NFC (precomposed) 3 code points 보·고·서 9 bytes (3B each)
NFD (decomposed) 6 jamo ㅂㅗ ㄱㅗ ㅅㅓ 18 bytes (6B each, 9B with a final consonant)

NFD is 2–3× larger than NFC (3× for syllables with a final consonant). For a name of Korean syllables that all carry a final consonant:

  • NFC 28 chars = 84 B (plenty of room)
  • NFD 28 chars = 252 B (right at the 255 edge)

Not just Korean

The "NFD makes it bigger" effect applies to any script with combining characters — Korean is merely the most extreme:

Char Language NFC → NFD Ratio
Korean 3 → 9 B 3.0×
Japanese (voiced kana) 3 → 6 B 2.0×
Vietnamese 3 → 5 B 1.67×
й Russian 2 → 4 B 2.0×
é French / German / Spanish… 2 → 3 B 1.5×
· A · Han / Latin / Thai no change 1.0×

The engine is language-agnostic — it just measures NFD bytes — so Vietnamese and French filenames are caught the same way.

What happens in the sync pipeline

  • Say you sync a local folder ↔ a remote (e.g. remote:/volume1/homes/johndoe/MyDocuments).
  • The remote filesystem (a NAS's btrfs/ext4, a Linux server, …) typically enforces NAME_MAX 255, PATH_MAX 4096 (bytes).
  • macOS stores new Korean filenames as NFD, and even NFC files can decompose to NFD on edit.
  • Mobile viewers and sync clients may each normalize differently.

So even if it looks short on Windows today (NFC), the moment it decomposes to NFD somewhere in the pipeline, the byte length spikes past NAME_MAX and the sync errors out. That's why this tool always measures the NFD (worst-case) byte length.

⚠️ Normalizing won't fix it

If an over-limit file is already NFC, its NFD-equivalent length still exceeds the limit — so converting to NFC does nothing. The only real fix is to rename it shorter (trimming ~7 Korean chars brings NFD back under 255).

Features

  • 🔎 NFD worst-case — judges every name and full path by its NFD-normalized UTF-8 byte length
  • 🌐 Language-agnostic — Korean, Vietnamese, accented Latin, and any combining-mark script
  • 🧩 Sync-app agnostic — Synology Drive, Dropbox, whatever; just point it at the remote path prefix
  • 🚫 Exclusions — noise dirs like .git and @eaDir (Synology cache) are skipped by default, configurable
  • 🛟 Read-only — never modifies, moves, or deletes a file
  • 📋 Report + JSON — human-readable output plus a summary JSON for scripting
  • 🧭 Roadmap — resident macOS/Windows tray & menu-bar app, YAML config, icon-state alerts (see Roadmap)

Install & use

Tray / menu-bar app

Prebuilt binaries are on the Releases page. They are currently unsigned, so macOS and Windows warn on first launch — steps below. On first run the app writes a default config (Settings… in the menu opens it); point watch at your synced folder, then use Scan now. The icon color shows the result and clicking an over-limit file reveals it in Finder/Explorer. It's a menu-bar agent — look for the ruler icon, there's no Dock icon.

macOS — Homebrew (via the tap):

brew tap msjang/tap
brew install --cask pathguard   # clears the quarantine flag for you

macOS — manual: because the app is unsigned, macOS blocks it with "can't be opened" until you remove the quarantine flag.

  1. From Releases, download Pathguard-macos-universal.zip (one build for both Apple Silicon and Intel) and unzip it.

  2. Move Pathguard.app to /Applications.

  3. Clear quarantine and open:

    xattr -dr com.apple.quarantine "/Applications/Pathguard.app"
    open "/Applications/Pathguard.app"

    Or without Terminal: right-click the app → OpenOpen; on recent macOS go to System Settings → Privacy & Security → Open Anyway.

Windows — manual: download Pathguard-windows-amd64.zip, unzip, run Pathguard.exe. If SmartScreen warns: More info → Run anyway.

Build from source (any OS, requires Go 1.25+):

git clone https://github.com/msjang/pathguard.git
cd pathguard
go run ./cmd/pathguard-gui      # or: scripts/build.sh → bin/pathguard

Launch at login (macOS): open the menu and toggle Launch at login. It installs a per-user LaunchAgent at ~/Library/LaunchAgents/io.github.msjang.pathguard.plist that starts pathguard at each login; toggling it off removes the file. It starts the app once at login (no auto-restart), so Quit keeps working. Nothing else — no brew step and no Login Item — is needed; Homebrew intentionally can't register autostart. (macOS only for now.)

Config file location — the first match wins:

  1. $PATHGUARD_CONFIG — an explicit file path (a leading ~ is expanded)
  2. $XDG_CONFIG_HOME/pathguard/config.yml — if that variable is set
  3. ~/.config/pathguard/config.ymlonly if it already exists
  4. the OS convention, otherwise:
    • macOS: ~/Library/Application Support/pathguard/config.yml
    • Windows: %APPDATA%\pathguard\config.yml
    • Linux: ~/.config/pathguard/config.yml

So if you prefer the dotfile layout on macOS or Windows, just move the file:

mkdir -p ~/.config/pathguard
mv "~/Library/Application Support/pathguard/config.yml" ~/.config/pathguard/config.yml

Rule 3 checks existence rather than winning outright, so configs already living in the OS location keep working untouched.

CLI (Go)

A pure-Go, dependency-free binary — handy for one-off checks, scripts, or CI (it exits non-zero when anything is over the limit).

go run ./cmd/pathguard          # scan configured watches, print a report
go run ./cmd/pathguard --json   # summary JSON
go run ./cmd/pathguard --root ~/Docs --remote-prefix /volume1/homes/johndoe/MyDocuments

Configuration

Both the app and the CLI read the same YAML config (full schema in prj/ADR.md, ADR-0003):

watch:
  - root: ~/Documents
    remote_prefix: /volume1/homes/johndoe/MyDocuments  # remote absolute root, for PATH_MAX
limits:  { name_max: 255, path_max: 4096, warn_ratio: 0.80 }  # 0.80 = warn from 80%
exclude: [.git, node_modules, "@eaDir", "#recycle"]          # noise dirs to skip
notify:
  thresholds: { yellow: 1, red: 10, warn: 1 }   # icon color by over/warn counts
menu: { max_inline: 10 }                          # worst-N shown inline in the menu
ui:   { language: auto }                          # auto (system locale) | en | ko

Default exclusions: .git, node_modules, @eaDir, #recycle, #snapshot, .DS_Store, .Trashes, .Spotlight-V100, .fseventsd, $RECYCLE.BIN, System Volume Information

CLI flags: --config <path>, --root <dir>, --remote-prefix <p>, --json.

The remote path length is the bottleneck — set remote_prefix to your actual sync target.

How it works

  • Measures every name and full path as UTF-8 bytes after NFD normalization
  • Name NFD > NAME_MAXover; WARN…limit → warning
  • Full path (remote absolute) NFD > PATH_MAX → over/warning
  • Excluded folders are not descended into; excluded files are skipped

Roadmap

Growing into a resident tray / menu-bar app for macOS (Intel & Apple Silicon) and Windows:

  • Single-binary distribution — Go rewrite, one dependency-free executable (macOS arm64/amd64, Windows amd64)
  • Resident tray / menu bar — click to configure watched folders, interval, schedule, exclusions
  • YAML config file — multiple watch folders, limits/warn ratio/exclusions/alert style
  • Icon-state alerts — icon color reflects state: gray (idle), blue (scanning), green (clean), yellow (warnings or some over), red (many over) — all thresholds configurable
  • Localized UI — follows the system locale by default; English and Korean selectable

Design docs live in prj/ (PRD · ADR · TASKS · NOTES).

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages