cli.sgit.ai / Research / AC-L-001 / TM-R36 — characters outside [a-zA-Z0-9/\_.- ] in paths: what really happens

AC-L-001 / TM-R36 — characters outside [a-zA-Z0-9/\_.- ] in paths: what really happens

Date2026-10-13 · 12:49 UTCKindfindingTopicssecurity paths bugWritten bythe sgit-ai CLI team (Claude Code sessions), for Dinis CruzSourceteam/humans/dinis_cruz/claude-code-web/10/13/AC-L-001__non-ascii-paths__refuse-dont-rewrite.md in SGit-AI__CLI at 5597526. Published here as written; the CLI repository is where the work happened.

Status: documented, not fixed in this push (your call, 13 Oct). Reproduced on dev at 81a4176 with the Python API over the in-memory host. The fix proposal below follows your instinct: raise an alarm, never rewrite silently. It is narrower than "refuse every such character", for the reason in §3.

It is worse than the round-1 record (review/findings/audit-context-local.json AC-L-001) says. Two different things share the bug: where the vault lives (the host path of the clone) and names inside the vault (file and folder names in the trees).

1. The vault root: broken, and silently in the wrong place

Safe_Str__Vault_Path and Safe_Str__File_Path both use [^a-zA-Z0-9/\\_.\- ] in replace mode: every other character becomes _. The working directory of the clone, pull, fetch, push and move workflows (directory : Safe_Str__File_Path), Vault__Context.vault_path and Vault__Storage go through them; other code uses the raw str. So one vault gets two roots.

Step, under /tmp/x/josé/vaultWhat happened
initthe working copy and .sg_vault/{bare,local} are under josé/vault, and a second tree jos_/vault/ is created next to it
commit, statusok, clean
first pushVault__Push_Conflict_Error: "the branch moved on the server while this push was in flight" — false; nothing was pushed
clone into josé/clonereports success; the whole clone is written to jos_/clone; josé/clone does not exist
pull in josé/cloneFileNotFoundError: …/josé/clone/.sg_vault/local/vault_key
second commit + push in josé/vaultFileNotFoundError: …/jos_/vault/.sg_vault/work/pull-…/.tmp-…

It is not only accents. What the type does to real folder names:

PathBecomes
/Users/x/Dropbox (Personal)/v/Users/x/Dropbox _Personal_/v
/Users/x/OneDrive - Acme, Inc/v/Users/x/OneDrive - Acme_ Inc/v
/home/first.last@corp/v/home/first.last_corp/v
/home/u/my+vault/home/u/my_vault
C:\Users\Zoë\vC_\Users\Zo_\v

The last row needs a check on Windows. The colon of every drive letter is rewritten, so if any of these fields carries an absolute Windows path, every Windows user is affected, not just users with accented names. I could not run Windows here.

Security reading: a write lands in a directory the user never named. Two roots that sanitise alike share one store (josé/ and josè/ and a real jos_/), so one vault's objects, refs and work/ files can mix with another's. A local process (A6) that can create jos_/ next to josé/ decides where the store goes. Likelihood M (any European or company folder name, any Dropbox or OneDrive folder); impact H (a false "conflict", a clone in the wrong place, failures on the next command). I have raised TM-R36 from M / M to M / H.

2. Names inside the vault: the core is right; the display and --path are not

Tree entry names are encrypted strings, not Safe_Str fields, so the sync path never touches them. With café.md, naïve dir/résumé.md, a+b@c.md, 日本語.txt and a caf_.md beside café.md:

StepResult
status, commit, push, cloneevery file round-trips byte for byte, with the exact name; café.md and caf_.md stay two files
edit in the clone, push, pull in the first cloneok; content and name exact
stash / stash popok
sgit diff (Schema__Diff_File.path)lists caf_.md and na_ve/r_sum_.md: the display lies
sgit clone --path naïve (scope_paths : list[Safe_Str__File_Path])records the scope na_ve, checks out nothing, and says nothing
history file lists, publish entries, stash metadatatyped with the same class, so they rewrite the same way (not each one run)

So "what happens if the vault creates that": another client, or the web UI, can put such a name in a vault. This CLI clones, edits and syncs it correctly. Its listings show the wrong name, and a scoped clone of that folder is silently empty.

Hostile names in a vault are a separate question with an existing answer: Vault__Path_Guard refuses absolute paths, .. on either separator, .git / .sg_vault, and writes through a symlink at checkout (TM-F series). Nothing in this finding weakens that.

3. The decision proposed for the next push

The rule: a type that names a place must refuse what it cannot represent, never rewrite it. A rewritten path is a silent data change: the user asked for one place and got another. Worth a line in CLAUDE.md's Type_Safe rules.

  1. Vault roots: the alarm now, the real fix right behind it.
  1. Names inside a vault: do not refuse them. They work today, other clients create them, and refusing would lock people out of their own vaults. That is the lock-out lesson of R1 and TM-F35. Instead:
  1. Where an alarm is right for content: names that will not survive another OS. Add a check (rule 9 puts it under check), and a warning at commit time for new names only (an alarm, not a refusal). It covers:
  1. Tests that land with the fix:

Sizes: the stop-gap is under a day. The type change is a day or two, because the type is used by every path field; the check is about a day.

Where this is recorded

This page as markdown · its source, admin/content/articles/non-ascii-paths-refuse-dont-rewrite.md