🤖 feat: back up Mux settings to a git repository - #3767
Conversation
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4b7a54c1d0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Thanks, three of the four were real. Fixed with red-green verified regression tests (removed each guard, confirmed the intended test fails, restored). 1. Windows separators in manifest paths ( 2. Managed paths deleting the repository cache ( 3. Bare 4. Native titles (
|
|
@codex review Please take another look. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e9a25a8260
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
All five addressed, each with a red-green verified regression test (removed the guard, confirmed the intended test fails, restored). 1. 2 & 3. Symlink traversal on read and restore — fixed. I confirmed the premise before fixing: git stores symlinks as mode 120000, so a backup repository can carry one, and Added one shared helper, 4. Validate payload contents before writing — fixed. 5. Non-fast-forward mapped to drift — fixed. Correct, and the cause was exactly as described: One note on how I tested it, since it changes the claim: I first tried to force the drift through the real adapter by moving the remote between pushes, and the push succeeded. That is not the bug you found, it is by design:
|
|
@codex review Please take another look. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e9a25a8260
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
CI's Root cause. void mock.module("@/version", () => ({ VERSION: "test-version" }));CI runs all unit files in one Bun process, so that module mock reaches this feature. This mattered beyond the test: any build whose version metadata is unavailable would have written a manifest that this code cannot read, i.e. an unrestorable backup. Fix. One correction to my own work. My first attempt also relaxed Local validation: 48 backup tests, 5 UI tests,
|
|
@codex review Please take another look. |
|
Both correct, and both were consequences of my own earlier fixes. Fixed. 1. Configured token never reached git ( 2. Override shortcut still gated on Local validation: 49 backup tests, 5 UI tests, Note on the
|
|
@codex review Please take another look. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5221090a2e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review Addressed all four findings from the previous round in 8b6b900 and 9700219, each with a red-green verified regression test:
Replies are on each thread and the threads are resolved.
|
|
@codex review One more preview correction pushed in fb41dd9: restore merges preferences into the local config rather than replacing a file, so preview now compares the merge result. A backup whose preference values all match locally no longer reports
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fb41dd907c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Two more fixes from a self-review pass, pushed in 8babbc6 and 0050d1b. Both are red-green verified. Restore was dropping machine-local preferences (data loss). Redaction and mode gaps. Bare One deliberate non-change, so it is on the record: a redaction marker makes the whole JSONC scalar locally owned, so a non-secret edit the backup made inside that same string is not restored. This is intentional. Splicing a local credential into backup-controlled text would let a tampered backup move that credential to a different host or binary. The policy is stated at the restore rule and pinned by a test. @codex review
|
|
@codex review Round addressed in c00444e (plus 8babbc6 and 0050d1b, pushed just before this review landed):
Every fix is red-green verified. Backup suite is 57 tests,
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c00444e125
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review Both findings confirmed and fixed in 6e60656, each red-green verified:
Backup suite is 58 tests,
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6e60656f6b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review All four findings confirmed and fixed in e667e5c, each red-green verified:
Backup suite is 60 tests, Separately, the earlier
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e667e5c173
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review All four confirmed and fixed in 8382ddb, each red-green verified:
Backup suite is 62 tests,
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8382ddbc66
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review All three confirmed and fixed in b74a6e3, each red-green verified:
Backup suite is 64 tests,
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b74a6e3888
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review Fixed in 2b94f45. This was a straight violation of the self-healing rule you cited: rehydration parsed the local The guarantee is pinned by "restores over a malformed local MCP config", red-green verified by removing the try/catch. I also wrote a test for the early return and then deleted it: with the try/catch in place that branch is behaviorally unobservable, so the test passed with the code removed. I would rather have no test than one that cannot fail. Backup suite is 65 tests,
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2b94f4549e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review Both confirmed and fixed in 64baf00, each red-green verified:
Backup suite is 67 tests,
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4db1e5423e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The scp-like predicate matched the drive-letter colon in a remote such as C:\backups\mux.git, so the ladder reported the ssh credential and the Backup UI claimed an SSH key or agent was used for what git treats as a local path. Excluded on Windows only, mirroring git's has_dos_drive_prefix. Verified that git elsewhere really does read C:/repo as scp-like and dial host `C` (GIT_SSH_COMMAND runs), so an unconditional exclusion would drop the ssh rung from a remote that needs it.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f2c3d22b5c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Every restore left its safety snapshot behind forever; the only cleanup path handled a snapshot whose write failed. Each one is an unredacted copy of the whole local payload, so repeated restores grew the cache without limit. The three newest are kept, so the recovery point a restore depends on and its predecessors survive. Snapshot directories now carry a timestamp in their name because mtime cannot order restores that land in the same millisecond, and only snapshots older than the one just written are candidates. The reap runs outside the write's own failure handling, so reclaiming disk cannot delete the recovery point.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7ce70fb7d8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The retention pass reaped by age, but withRepoLock is per repository, so restores of other repositories run concurrently. Once enough newer snapshots existed, an in-progress restore's snapshot could be deleted and that restore would then hand back a snapshotPath that no longer existed. A snapshot is now reapable only once its own restore has returned, marked by a sibling `.released` file written in a finally block. An unmarked snapshot is either in progress or was killed partway; neither is distinguishable from outside, so both are left alone. Leaking a snapshot the user can delete is recoverable, deleting a live restore's only recovery point is not. Ordering no longer decides safety, only which released recovery points to keep, so same-millisecond stamps can no longer change what is retained.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5b4bb89f1d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
writeSafetySnapshot writes with portable: false so a snapshot can hold names only this filesystem must accept, such as skills/a:b.txt or both Foo.md and foo.md. readBackupPayload applied the cross-platform rules unconditionally, so it rejected the snapshot as INVALID_BACKUP: the copy kept for recovery could be written but never read. The reader takes the same portable option the writer does. A repository payload still gets the portable rules, because another platform has to write it out. Also corrected the comment that claimed readBackupPayload could already read a snapshot. Retention keeps a returned snapshotPath readable for RETAINED_SNAPSHOTS-1 later restores, now covered by a test.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 50242cfa47
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A GIT_SSH_COMMAND or core.sshCommand starting with a double-quoted executable, which is how Windows spells a path with spaces, was tokenized at the first space. Two consequences: the variant went unrecognized so no BatchMode was added and prompts stayed live, and when GIT_SSH_VARIANT=ssh forced the flag on, the option was inserted inside the quoted path and git failed outright. The corruption was a regression from moving the flag ahead of the configured options. Both the variant check and the insertion now read the same program token, which understands single quotes, double quotes, and a bare word.
The released marker is written in a finally block, which runs before the restore's promise settles, so a concurrent restore of another repository could still count and delete a snapshot whose caller had not received the path yet. No marker placement fixes that, because no point inside the service is after the caller resumes. The service knows what no marker can: which snapshots belong to restores that have not returned. Reaping now happens only when none are in flight, which closes the window for this process. The marker still covers another process, whose in-flight restores this set cannot see.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2129c0463f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The double-quote decoder from the previous commit treated every backslash as an escape, so "C:\Program Files\OpenSSH\ssh.exe" decoded to C:Program FilesOpenSSHssh.exe. Variant detection then missed OpenSSH and added no BatchMode, leaving prompts live until the backup timeout: the same symptom the quoting fix was meant to remove. Only the two characters double-quoted syntax escapes are decoded. Variant detection also splits on both separators instead of path.basename, because a core.sshCommand written on Windows is read verbatim wherever the config is used and basename only knows the host's separator.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ee11aa8ac7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Git decodes userinfo before invoking ssh, so `ssh://user%3Apw@host` and `ssh://user:pw@host` reach ssh as identical bytes (verified against git 2.54 with a fake ssh that prints its argv). The raw-text check only looked for a literal colon and `new URL` leaves `%3A` in `username` with an empty `password`, so the encoded spelling of a rejected URL was persisted.
The repository lock is keyed per repository and branch, so two operations on different tuples both reached the one Mux root every payload adapter reads and writes: a push could export a half-restored root and publish it as a complete backup, and two restores could interleave their write loops. A second lock on a single key now wraps only the local payload work, taken inside the repository lock everywhere so the order is fixed. Git and network work stays parallel across repositories. This also subsumes the narrower unreturnedSnapshots guard, since no other in-process restore can be inside the critical section when a snapshot is reaped.
|
@codex review |
|
Codex Review: Didn't find any major issues. Swish! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
One malformed escape made decodeURIComponent throw for the whole string, so the previous check judged the raw text and accepted a credential git still decodes: `ssh://user%zz%3Apw@host` reaches ssh as `user%zz:pw@host` (verified against git 2.54 with a fake ssh that prints its argv). Scanning for %3A is both correct and simpler. Git decodes in one pass, so `user%253Apw` stays a username and remains accepted.
The name stamp has millisecond resolution, so restores landing in one millisecond tied and ordering fell back to mkdtemp's random suffix. Reaping could then delete a newer recovery point and keep an older one, and the retention test failed 8 times in 40 runs. A monotonic sequence in the name breaks the tie: 0 failures in 40 runs after this change. Retention safety still comes from the .released marker, not from ordering; the order only chooses which returned snapshots to keep.
Double quotes also escape `$`, a backtick, and a newline, so the parser comment claimed more than the code does; it decodes quotes and backslashes only. The discard guard proves the shape of what sits at the cache path, not that it is the same clone ensureCache validated.
|
Heads up: this head moved after your approval of 1. The encoded-credential fix I shipped last round was incomplete ( My fix decoded the userinfo and looked for a colon. But So the credential still reached ssh. Scanning for function userInfoHasPassword(userInfo: string): boolean {
return userInfo.includes(":") || ENCODED_USERINFO_DELIMITER.test(userInfo);
}Git decodes in one pass, so 2. A 20 percent CI flake in snapshot retention ( The name stamp is millisecond resolution, so restores landing in the same millisecond tied and ordering fell back to 3. Two comments corrected ( The ssh parser comment claimed double quotes escape only quote and backslash. They also escape One thing I deliberately did not change.
|
|
@codex review |
|
Codex Review: Didn't find any major issues. Bravo. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Summary
Adds phase 1 of settings backup: a Backup settings tab that pushes a portable subset of
~/.muxto a git repository the user specifies, and restores it on another deployment. Backup and restore are manual, the repository is addressed by URL rather than a new sign-in flow, and MCP commands and URLs remain usable on a fresh device while literal header values stay redacted.Background
Mux user state lives entirely under
MUX_ROOTon one host, so a second deployment or a fresh install starts from scratch. The obvious idea, "sign in with GitHub and use the existing login", does not work today:serverAuthService.ts:12-18)read:usercopilotOauthService.ts:11-19)read:usergithub-copilot.apiKeyGH_TOKENforwarding (credentialForwarding.ts:28-30)Neither token can clone or push, and there is no GitHub App or
reposcope anywhere in the repo. Storing an OAuth token or PAT for backups, even a read-only one, is out of the question, so this PR adds no token input, no token storage, and no token pathway of any kind. Mux never holds token material itself: authentication is delegated entirely to credentials that already live on the backend host.Implementation
Credential ladder (
credentials.ts). One controlled attempt, then an ambient retry. The controlled attempt clears inherited helpers (-c credential.helper=) so the mechanism that authenticated is known and reportable in the UI:-o BatchMode=yesappended to any ambientGIT_SSH_COMMANDso a passphrase prompt fails instead of hanging, without discarding a host's ssh wrapper.gh auth status --hostname <host>succeeds:-c credential.helper='!gh auth git-credential'. This is the honest way to reuse an existingghlogin, and the token never passes through Mux.GH_TOKEN,GITHUB_TOKEN, and their enterprise variants are stripped from both the probe and the helper invocation, sinceghwould otherwise consume them ahead of its stored login and quietly turn this rung back into a token pathway.Every rung sets
GIT_TERMINAL_PROMPT=0,GH_PROMPT_DISABLED=1,GCM_INTERACTIVE=never. Repository settings reject credentials in URL userinfo and high-confidence query or fragment parameters before the URL can reachconfig.jsonor cache git metadata.Dedicated secret stores have no export path. This is enforced structurally, not by filtering:
payload.tswalks an explicit path allowlist, soproviders.jsonc,secrets.json,mcp-oauth.json,server.lock, andserverAuthSessions.jsonhave no code path into the payload. A closed-world test seeds all of them into a fakeMUX_ROOTand asserts none appear in the pushed tree. Free-form documentation still passes through the high-confidence scanner. Every exported MCP command requires exact-payload approval, and MCP URLs receive structural checks for raw, malformed special-scheme, and WHATWG-normalized authority userinfo plus credential-like query or fragment parameters, including low-entropy values. Any override is bound to the exact payload bytes the user reviewed.What syncs:
AGENTS.md,agents/*.md,skills/**(execute bits preserved, so skill scripts stay runnable),memory/global/**,mcp.jsonc(commands and URLs verbatim, portable runtime fields, header names, and secret references; literal header values redacted), andpreferences.json. Preferences use an explicit versioned allowlist becauseAppConfigOnDiskSchemais.passthrough(), so a denylist would leak every future key; the same reasoning excludes the record-typedai.providerOptionsproviders, whose schema isz.record(z.unknown()). Project-path-keyed and workspace-keyed entries are excluded as machine-local, andAGENTS.local.mdon the strength of its own.localmarker.appearance.editorConfigis excluded too: itscustomCommandreachesspawn(..., { shell: true }), so a restore from a repository someone else can write to would be a code-execution vector. Symlinks are never collected, and neither is any hidden file: one rule applied to every path segment keeps.gitobject stores,.envand its variants, and.netrcout of the recursive collections, where the secret scanner would not recognise a low-entropy value likePASSWORD=hunter2anyway.MCP commands and URLs are portable data. Bare stdio command strings and object
commandandurlvalues are stored verbatim so a fresh device can reconstruct its servers. Every exported command, recognizable secret, and structurally credential-bearing URL component blocks push until the user approves the exact payload digest. Restore separately derives every runnable command it would introduce, change, or enable from the exact bytes it would write and refuses withCOMMAND_APPROVAL_REQUIREDuntil the user approves that text. A command shadowed by a non-empty URL needs no restore approval becauseMcpConfigService.normalizeEntry()ignores it.Redaction metadata disambiguates literal marker values. New manifests persist exact JSON paths for values the exporter redacted, including an explicit empty list when
mcp.jsonchas no redactions. Restore treats only those paths as placeholders, so an approved command whose literal text is__MUX_BACKUP_REDACTED__remains executable data and still goes through command approval. Metadata is validated against the final serialized MCP document, duplicate manifest and JSONC keys are rejected, and projection-dropped keys cannot leave stale paths behind.Unrecognized MCP fields remain local.
McpConfigService.normalizeEntry()readstransport,disabled,toolAllowlist,url,command, andheaders, and ignores the rest, so a field likeenv: { "API_KEY": "hunter2" }is a credential nobody would notice leaving. Export projects exactly the fields Mux reads, at the types it reads them as; every other value becomes a marker and restore uses only the local value at that exact path. Portable-field lookups require an own property, so document-controlled names such astoStringandconstructorcannot resolve inherited validators.Header credentials never come from the repository. Export replaces literal header values with a redaction marker. A portable
{ secret: NAME }reference may remain in the repository projection, but restore keeps a header only when the local file already has a value at that exact path and the restored entry still points at the same endpoint. A changed endpoint or a fresh device with no local header value gets no credential, so a backup cannot redirect a locally resolved secret.Repository exports strip MCP comments, while restore preserves local-only definitions. A comment is prose the projection cannot inspect, so export reserializes the projected values instead of publishing the user's text. Restore applies edits in place, keeps destination-only server definitions, and carries their raw JSONC comments and trivia with them. Same-name definitions from the backup remain authoritative.
Older marker-based backups remain safe. A manifest without redaction-path metadata is treated as a legacy backup, so marker-bearing values still rehydrate from local state when available. If no usable local value exists, restore removes the executable marker or unusable server instead of passing marker text to the runtime. Existing repositories remain readable without a schema bump, while their next write adds exact metadata and prevents marker ambiguity.
Only documentation publishes from a recursive collection without review.
skills/**andmemory/global/**hold whatever the user put there, and no content scanner can decide whether an arbitrary file is a credential, since{"password":"hunter2"}has no distinguishing shape. So the gate is structural: anything that is not.md,.mdx,.markdown, or.txt, plus any file whose path promises credentials or has the exact conventional stemauthorpasswd, is held back until the user approves that exact payload. The approval is a hash of every flagged path and its contents, so approving one payload cannot publish a different one another window wrote meanwhile.Everything lands in one managed subdirectory (default
mux/) so an existing dotfiles repo is a safe target, and a persistent cache clone under<MUX_ROOT>/backup-cache/makes the two-direction preview cheap. Both sides check sizes before reading, since a repository is written by anyone who can push to it and a payload is buffered whole. The clone is sparse, single-branch, and blob-filtered, so a dotfiles repo with substantial history elsewhere is neither materialized nor transferred, and a path this platform cannot create never blocks a backup. Local paths andfile:URLs use an upload-pack transport with filtering enabled for both clone and later fetches, preventing Git's local optimization from copying the full object database. Cache creation preserves the remote's SHA-1 or SHA-256 object format when the backup branch is missing, including zero-ref remotes, and SHA-256 clones retain their required extension during config rebuilding. Cache corruption cannot be enumerated, since an emptyHEAD, a truncated index, and a ref naming nothing all keep the shape a structural check looks for and only fail a later Git command. So any unrecognized failure discards the disposable cache and retries once rather than leaving Preview, Push, and Restore blocked. Refusals to touch content the cache cannot prove it owns are excluded from that rebuild, as are outages and origin mismatches, so neither a foreign path nor a healthy cache is deleted; the discard itself rechecks ownership at the point of deletion. Relative local repository paths are anchored to the stable parent of<MUX_ROOT>/backup-cache(MUX_ROOTitself), so terminal and desktop launches use the same repository andgit -Ccannot reinterpret the path from the cache. Before Git runs, Mux rejects symlinks and special files under.gitand replaces multiply-linked metadata with cache-owned inodes, so Git cannot rewrite an outside hard-link alias; new local clones also disable Git's hard-link optimization. The cache config is rebuilt from validated platform flags and known forced values on every use, so malformed retained settings cannot brick later retries. The payload is treated as bytes: end-of-line conversion is pinned off in the cache, because the manifest checksums what was written and a restore writes what it reads.Local payload work is serialized across repositories. The repository lock is keyed per repository and branch, but every payload adapter reads and writes the one Mux root, so a second lock on a single key wraps the local payload work and is always taken inside the repository lock. Without it a push could export a half-restored root and publish it as a complete backup. Git and network work stays outside it, so different repositories still run in parallel. Serialization is process-local; cross-process locking is out of scope.
Validation
Beyond unit tests, a service-level integration test drives a real bare repository end to end and asserts the security invariant on bytes that actually reached a remote. Dogfooded all 12 planned scenarios against a live dev server and a real bare repo, which found 8 defects that are fixed here:
Defects found and fixed during validation
BackupServicedeclared its two dependency interfaces but nothing implemented them, so the service container did not compile.manifest.exportedAt, so an unchanged backup produced a commit that only churned metadata.preferences.jsonas added, because preferences live in config and have no local file to compare against.createBackupPayloadthrew on detected secrets before the service could apply the user's override, making the override unreachable.isPortableReferencetreated$VAR/${VAR}/env:VARheader strings as portable and exported them verbatim, butMCPHeaderValueisstring | { secret }, so Mux never interpolates those and they are literal credentials.mcp.jsoncthroughJSON.stringify, discarding the document the user had just previewed. Restore now appliesjsoncedits in place. (The export deliberately does reserialize, so that no comment ships.)ENOENTcontaining the internal cache path.Each fix has regression coverage. The current MCP portability work covers verbatim commands and URLs, literal-header redaction, exact-payload publish approval for every command, exact runnable-command restore approval, literal redaction-marker commands, strict persisted metadata validation, metadata-less legacy backups, prototype-colliding portable-field names, projection-dropped
__proto__keys, disabled URL-to-stdio transitions, local-only server preservation, malformed map rejection, JSONC comment retention, low-entropy and malformed special-scheme URL credential gating, conventional credential filename stems, special-file-safe local reads, and a real bare-repository push. Repository URL tests cover credential-bearing userinfo, query, and fragment forms while retaining benign parameters.make static-checkpasses, and the configured Storybook story was verified at 1200 px and 390 px with no horizontal overflow.Risks
Restore mutates user config, but it is deliberately not a mirror: it overwrites paths present in the backup, preserves local-only files and MCP servers, and writes a safety snapshot to
<MUX_ROOT>/backup-cache/restore-<id>/first. Restore planning rejects existing directories and special files before any destination is opened for writing. Optional local MCP reads preflight special files and use a nonblocking checked open so a path replacement race fails instead of hanging restore. Commands and URLs are repository-controlled input, so every exported MCP command and credential-bearing URL component requires exact-payload approval before publication, and changed runnable commands require separate exact-text approval before restore. Literal header values are never exported, and a local header is restored only to its original endpoint. Pushes use--force-with-leaseagainst the commit the preview read, so a remote that moved is reported as drift rather than overwritten, and a server-side denial such as a protected branch is reported as itself.Blast radius outside the feature is small: the shared-file edits are the settings tab registration, an added
settingsBackupconfig key, and the service container wiring.Out of scope
Continuous/automatic sync, drift indicators, three-way merge, plans and project-scoped memory, and mirror-mode restore. A "sign in with GitHub" flow is rejected, not deferred: it would require Mux to hold an OAuth token, and storing one, even read-only, is out of the question.
Generated with
mux• Model:openai:gpt-5.6-sol• Thinking:xhigh• Cost:$1249.89