Repository navigation
Releases: bermudi/stitch
Releases · bermudi/stitch
Release list
v0.16.0 — templates can include repo files via include()
Immutable
release. Only release title and notes can be modified.
Added
- Templates can include repo files:
{{ include("path/from/repo/root") }}.
The motivating case is the AGENTS.md fan-in (one shared "hub" file linked
into many agent config dirs): a tool that reads exactly one file and has no
import syntax previously forced a choice between linking the hub verbatim
or forking it — and a fork silently stops receiving hub edits. A.tmpl
source can now compose hub + delta ({{ include("agents/AGENTS.md") }}
followed by tool-specific lines), rendered into the staged file the target
links. Include semantics are deliberately narrow: the argument must be a
safe repo-relative fragment (no.., no leading/), must not live under
.stitch/or.git/, must reach a regular file without passing through a
symlink (one hop only, the same rule asadd --source), and must be valid
UTF-8. Included text is embedded verbatim — never rendered — so recursion
cannot occur and.tmplarguments are rejected outright. Fresh renders
re-read included files on everyapply/diff/doctor, so editing an
included hub shows up as acontentdiff and a doctor staging-drift
warning until the next apply converges it.
v0.15.2 — self-update no longer fails with Text file busy
Immutable
release. Only release title and notes can be modified.
Fixed
self-updateno longer aborts withText file busywhen verifying the
staged binary. The installer held its ownO_RDWRstaging handle open
while trying to execute the staged file, and Linux refusesexecveon any
file with an open write descriptor (ETXTBSY) — so every install failed
withstaged binary cannot start on this host: Text file busy, left the
previous binary untouched, and suggested retrying, which could never help.
The write handle is now dropped and the staged file reopened read-only
before the probe runs. Production always used the real exec-based probe,
but the tests stubbed it out, which is why the bug survived review; a new
regression test mirrors theETXTBSYprecondition by rejecting the install
while any write descriptor still points at the staged file.
v0.15.1 — doctor sources-only suppression and add --source files→sources migration
Immutable
release. Only release title and notes can be modified.
Fixed
doctorno longer warns "directory is empty" on sources-only stores. A
sources-only store (the v0.14 fan-in consumer shape) has an intentionally
empty store dir — every file it links lives elsewhere in the repo, declared
viasources. Theempty-storecheck keyed off directory geometry, so it
fired a false-positive warning whose hint ("add files or remove the store")
pointed at the wrong remediation. It now suppresses when the store (or any
named target) declaressources, and the hint for a genuinely empty store
mentionssourcesas a third option.add --sourcenow migrates an existingfilesentry in one atomic write.
Adding asourcesentry for a target name that was previously afiles
entry used to error with "sources key is also listed in files; remove one" —
the only path was hand-editingstate.tomlor aremove+add --source
round-trip that briefly unlinks the live target link.add --sourcenow
drops the matchingfilesentry and inserts thesourcesentry in a single
state.tomlwrite (the files→sources fan-in conversion the v0.14 plan
envisioned), prints a "migrated fromfilesentry" notice, and reports
migrated_from_files: truein JSON. The filesystem link is untouched; the
nextapplyrepoints it.
v0.15.0 — add stitch self-update command
Added
stitch self-update— checks the latest stable GitHub release, verifies
the matching SHA-256 sidecar, narrowly validates the archive and ELF target,
runs the staged binary's--versionto catch incompatible builds, then
atomically replaces the running executable without dropping ordinary
ownership/mode metadata.--checkis metadata-only, and JSON output reports
up-to-date,newer,update-available, orupdated.
Fixed
- Post-rename directory-sync failure no longer reports a failed update.
When the atomic rename succeeds butfsyncon the parent directory fails,
the command now exits 0 withstatus: "updated"and a warning in the JSON
envelope (andwarning:on stderr in text mode). Previously this returned a
non-zero exit with no success JSON, which could mislead an agent into
retrying a no-op or reporting a failed update despite the binary being
installed.
v0.14.2 — harden $HOME displacement, FIFO hang, and quadratic config checks
Fixed
stitch add ~can no longer relocate$HOME.addnow rejects the resolved home directory (and its~/-spelled variants) before any filesystem mutation, with a clear usage error. Previously the adopt path moved$HOMEand then failed an internal home-identity check whose rollback was provably unable to restore it, leaving the entire home stranded inside the repo.apply --forcecan no longer displace$HOME. A whole-directory store withtarget = "~"under--forcepreviously renamed$HOMEto$HOME.bakand replaced it with a repo symlink, reporting success. Directapplyand plan execution now refuse to back up and link the resolved home directory.applyno longer hangs on a FIFO config/state file. The snapshot loader opens withO_NONBLOCK, so a no-writer FIFO fails fast with the same non-regular-file error other commands already produce, instead of blocking forever onopen(2).- Link-collision and plan-source resolution are no longer quadratic.
check_link_path_collisions,check_link_name_collisions, and same-store target-overlap validation now use index/trie-style ancestor lookups; plan construction resolves each store's link sources once. A 20,000-filepatterns = ["**"]store now resolves in ~1.4s (was >120s).
v0.7.1 — 2026-06 trust-review safety fixes (P0/P1/P2)
Trust & safety
Closes all seven remaining items from the 2026-06 trust review. Each
upholds the core contract: never surprise the user with data movement,
exposure, or silent replacement.
- P0 — nested ignore promotion no longer writes through foreign parent
symlinks. Aforeign_ancestorguard inapply_file_entryrejects
nested link creation when a parent symlink resolves outside the repo,
for both dry-run and real operations. - P1 —
editrejects path traversal.match_target_to_source
validates relative target fragments withis_safe_fragment, rejecting
.and..components before joining onto the store directory. - P1 —
apply --planrejects parent symlinks that resolve into the
repo.resolved_target_points_into_repoinplan_execblocks
executable plans from clobbering repository content through symlinked
parents, closing the TOCTOU gap from the P0 fix. - P1 — ignore promotion preserves source symlinks.
resolve_files
now acceptsis_symlink()entries, andcreate_link_to_entrylinks
to the source symlink path without canonicalizing — so relative,
absolute, and dangling symlinks match whole-dir behavior after
promotion to file mode. - P1 —
doctorallows mutually-exclusivewhentargets sharing a
path.WhenClause::are_compatiblemakes duplicate-target detection
when-aware, matching SPEC's documented laptop/server multi-target
layout. Genuine duplicates (compatible or absentwhen) are still
reported. - P2 — init/migrate overwrite guards detect dangling symlinks. The
refuse-existing-path guards now usesymlink_metadatainstead of
Path::exists, so a danglingstate.toml/stitch.toml/.bak
symlink is refused rather than silently replaced by a regular file. - P2 —
migratevalidates the split state before writing. A v0.2
entry the new validator rejects (e.g.files = ["./bashrc"]) now
fails fast before any file is written or the legacy config is backed
up, instead of stranding the user with unloadable state.
All changes are covered by unit + integration tests. cargo fmt,
cargo clippy -D warnings, and the full suite (91 unit + 213 CLI) are
clean.
v0.7.0 — agent interface — JSON envelope, plan/apply --plan, error taxonomy
Features
- Typed error taxonomy (
StitchError/FailureClass): 14 stable exit
codes (0–13) with class names, messages, and resolution hints. Text mode
printshint:lines;--jsonenvelopes includeclass,code,message,
hint, anddetails. - JSON envelope with a stable
schema: 1. Global--jsonflag on
status,list,doctor,prune,render,apply,diff, andplan.
The envelope (includingerror) goes to stdout; the exit code stays honest;
stderr is reserved for hook/subprocess output. stitch render <store>/<file>: read-only in-memory template render to
stdout;--jsonemits{source, link_name, sha256, content}.stitch plan: captureapply's exact op list as astitch/planJSON
file. Includes staged-render hashes, config hash, and platform fingerprint.
Text mode emits the raw plan;--jsonwraps it in the envelope.stitch apply --plan <file>: verbatim plan execution with preflight +
per-op precondition re-checks + stale-plan detection. Aborts at the first
failed op.--dry-runruns validation with no mutations.--planis
incompatible with--onlyand--force(usage error, exit 2).
Trust & safety
- Plan files are untrusted input.
apply --planvalidates every op
against the pinned config: path traversal rejection, backup path validation
(same directory as target), source under repo, target under configured
target, and.stitch/render/gitignore guard. - No plaintext in plans.
stage_renderops pin only the SHA-256 of the
approved render; rendered content is not stored in the plan. - Stale-plan refusal. Config edits,
state.tomledits, platform drift, or
render-hash drift causeapply --planto exit 12 before any mutation. - Honest exit codes for agents.
diff/planexit with the conflict/error
class;applyaggregates per-entry classes (single → that code, multiple →
11);doctorexits 13 on error-level findings.
Internals
- Refactored
compute_plan.apply,diff,plan, and the JSON/text
renderers all consume the samePlanstruct, so the two views cannot drift. - New dependencies:
serde_json,sha2.
v0.6.0 — templates, edit <entry>, import, staged-render trust foundation
Features
- Templates. Files ending in
.tmplare rendered with minijinja and
symlinked from.stitch/render/<store>/...(inside the repo, so
points_into_repo/remove/prunekeep working). Context:
{{ hostname }},{{ os }},{{ arch }},{{ distro }},{{ shell }},
{{ vars.key }},{{ env("VAR") }}(hard-fail) /{{ env("VAR", "default") }}.
Whole-dir stores containing any.tmplpromote to per-file links. State
records source names;resolve_entrystrips the suffix for the link target. - Trust foundation for staging.
initappends.stitch/render/to
.gitignoreand creates the staging root at0700; renders are0600.
doctorerrors if the gitignore entry is missing. Rendering is in-memory
only (no tempfile under a default umask). diffcontent dimension for templated entries: fresh render vs staged.
Staging is hash-gated (preserves mtime) and reconciled onapply/remove.
applyalso removes stale stitch-owned file-mode links when a source is
deleted or renamed; foreign symlinks remain untouched.stitch edit <entry>opens a store or target's repo source (the.tmpl
when present) — never the staged render. Config-based; works pre-apply.stitch importscans for existing repo-pointing symlinks and registers
them instate.toml(sharesscan.rswithprune).
v0.5.0 — --repo flag + STITCH_REPO env var for out-of-repo invocation
Features
- Run
stitchfrom outside the repo. A global--repo <path>flag and
STITCH_REPOenv var let you operate on a repo withoutcd-ing into it.
Precedence:--repo>STITCH_REPO> upward cwd walk (unchanged default).
An override must point at a directory containing.stitch/— a typo is
rejected, not silently applied to the wrong directory.initis cwd-anchored
and ignores both (it creates a new repo in the current directory).
v0.4.1 — trailing-slash fix
Fixes
stitch addwith a trailing slash now works.stitch add ~/.config/alacritty/
used to fail at the link step with a confusing rollback error. Root cause:
symlink(2)rejects a linkpath with a trailing slash (the kernel treats it as
"must resolve to an existing directory", but the path doesn't exist yet when
we're creating the symlink).expand_homenow strips trailing slashes from
its result, fixing the issue for every caller (add,apply,scan,
config load). Root (/) is preserved.