Scribe applies the same repository path rules on every platform. Ignore rules control discovery of untracked files; they never hide tracked changes.
Canonical paths
A path is UTF-8, /-separated, relative, with no empty, ., or ..
components, at most 8192 bytes, components at most 255 bytes. Rejected on
every platform at add and submit time: backslashes, control characters,
< > : " | ? *, reserved device names (CON, PRN, AUX, NUL, COM1-COM9,
LPT1-LPT9, COM¹/COM²/COM³, LPT¹/LPT²/LPT³, with or without extension), components ending in a dot or
space, and any .scribe component immediately followed by _state (both
compared under the same fold as collisions). A change set additionally refuses
a file named .scribe, since every working copy holds that name as a
directory. Symbolic links, junctions, and reparse points are never followed,
added, or synced through; they are reported.
.scribe/ is Scribe's namespace inside the repository, not a reserved
directory. Only .scribe/_state/ is local, machine-specific workspace state,
hard-reserved by Scribe itself rather than by an ignore rule: scribe add .scribe/_state/... is refused as reserved local workspace state, and sync or
checkout may change anything else under .scribe/ but never deletes or
overwrites .scribe/_state/. Everything else under .scribe/ — at the root or
anywhere deeper, such as x/.scribe/flows/build.yml — is ordinary versioned
repository content. Secrets are never written anywhere under .scribe/, at any
level of the namespace.
Case and normalization collisions: the server keeps a shared snapshot index of
NFC-normalized paths using pinned Unicode 17.0.0 full case folding after NFD,
plus the NTFS dotless-i equivalence. ß, ẞ, and ss fold together, as do ſ
and s or ς and σ. Each branch pins its own root, and the server rejects a submission
that would create two paths mapping to the same key (path_collision),
including within one submission. Windows/NTFS and macOS/APFS would otherwise
map them to one file. Matching of ignore rules is case-sensitive on every
platform; lock rules, by contrast, match under this collision fold (see
lock rules).
Long paths: Windows paths over 240 characters are opened with the \\?\
extended-length prefix. A 4,000-character path is tested on Linux and Windows
(platform_behaviour.rs); macOS caps most syscalls at 1,024 bytes and is not
tested for long paths.
Executable bits are stored in file records; applied on Unix, stored only on Windows.
Ignore files
- Syntax: Git-style via the
ignorecrate's matcher, with comments, blank lines,\#and\!escapes,*,?, character classes,**, trailing-slash directory patterns, leading-slash anchoring to the rule file's directory, negation, UTF-8, spaces, LF or CRLF, trailing-space escaping. Unclosed character classes are literal (as in Git). A malformed rule (for example an invalid range) is reported rather than treating the file as empty. Status/scan retain available findings and exit 1 when discovery is incomplete; do not interpret that partial report as a clean workspace. - Sources and precedence, lowest to highest:
--global-ignore <file>; workspace-local.scribe/_state/ignore; versioned.scribeignorefiles from the root to the deepest applicable directory. Within one file the last matching rule wins. Deeper versioned files override shallower ones. - Excluded parents: an ignored directory is not traversed, so a negation in a
deeper file cannot recover its contents.
scribe ignore explainreportsvia_parentin that case. - Ignore rules affect only untracked discovery and recursive
add. Tracked files remain eligible for scanning;scribe status --scanreports their modifications and deletions regardless of rules. Rules never delete data and are not access control. Versioned ignore files are ordinary tracked files. - Scribe does not read
.gitignoreor global Git configuration, and does not apply implicit hidden-file rules or game-engine ignore templates. scribe add --force <path>overrides ignore matching only; it never overrides containment, the.scribe/_state/reservation, links, or name validity.scribe ignore explain <path>prints ignored/not ignored, the deciding file, line, and pattern, and whether a parent directory decided.
.scribeignore and .scribe/config.toml have different authority. Ignore files govern
local untracked discovery. Only the accepted locks section of .scribe/config.toml on main
governs mandatory locks across all branches; nested or locally edited lock
settings do not override it. Profiles in that same config are branch-scoped;
they do not change ignore behavior. See mandatory lock rules.
Fingerprints
The baseline records size and modification time as reported by the filesystem,
together with the moment that observation was taken. Equal fingerprints skip
hashing in status only when the recorded modification time is older than the
observation by the filesystem's timestamp granularity, so a same-length save
inside that window cannot pass as unchanged; status --verify always hashes.
Fingerprints are caches, not proof of unchanged bytes. Resolution differs by
filesystem (NTFS 100 ns ticks; some filesystems 1 or 2 s), so only exact
equality is meaningful.
Default status inspects opened and workflow paths only; --scan adds unopened
tracked changes and untracked discovery, and --verify hashes contents. A
directory at an expected file path, or a file blocking an ancestor directory,
is not_file. Unsafe links and inspection failures are reported as per-path
problems, not mistaken for ordinary missing files. See
status and scanning.
Concurrent external writers
Submit re-stats a file after hashing it and re-hashes every chunk range while uploading, so a file that changes during a submit is refused. A before/after stat is not an atomic snapshot: a writer that modifies a file between the final stat and the upload read is caught by the chunk digest check; a writer that modifies and restores bytes within one fingerprint tick is not. Editors should be quiescent during submit.