{"_id":"@5edmat/agent-sync","name":"@5edmat/agent-sync","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@5edmat/agent-sync","version":"0.1.0","type":"module","description":"Read, diff and safely apply AI coding tool configuration — Claude Code, Cursor and Zed — with atomic writes, comment-preserving edits and rollback.","engines":{"node":">=20.11.0"},"bin":{"agentsync":"bin/agentsync.js"},"exports":{".":"./dist/core/types.js","./platform":"./dist/platform/index.js","./core/types":"./dist/core/types.js","./adapters":"./dist/adapters/claude-code.js","./cli":"./dist/cli/run.js"},"scripts":{"build":"tsc -p tsconfig.json","typecheck":"tsc -p tsconfig.json --noEmit","typecheck:platform":"tsc -p tsconfig.platform.json --noEmit","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","cli":"node bin/agentsync.js","prepublishOnly":"npm run typecheck && npm run test && npm run build"},"devDependencies":{"@types/node":"^20.14.10","@vitest/coverage-v8":"^2.1.9","typescript":"^5.6.3","vitest":"^2.1.9"},"license":"MIT","keywords":["claude-code","cursor","zed","agent-skills","mcp","dotfiles","config","sync","cli","devtools"],"publishConfig":{"access":"public"},"types":"./dist/core/types.d.ts","_id":"@5edmat/agent-sync@0.1.0","_nodeVersion":"24.18.0","_npmVersion":"12.0.1","dist":{"integrity":"sha512-Wu/MJ5iAcMTqwKA5l7IJbyVLfgUG8E0SZUyhgS5R1ykKdv9lQZye2QgKBmBGWzJvKNf/zqioyZPkVH5AF6PWSQ==","shasum":"1e57d5124e33978dc1d5144c6151e88b4364c4fe","tarball":"https://registry.npmjs.org/@5edmat/agent-sync/-/agent-sync-0.1.0.tgz","fileCount":150,"unpackedSize":1266732,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCq5KQc2d4HOUzCGI4daUjytm+ia3U8Kcb5wjfhPpSQQgIgK+YWWL1r4h2EF80veIXdjb/ZIAAAj8UGQ/urDoVQqwM="}]},"_npmUser":{"name":"5edmat","email":"s.haroon@5edmat.com"},"directories":{},"maintainers":[{"name":"5edmat","email":"s.haroon@5edmat.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/agent-sync_0.1.0_1785381285273_0.10185496402992222"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-30T03:14:45.096Z","0.1.0":"2026-07-30T03:14:45.417Z","modified":"2026-07-30T03:14:45.687Z"},"maintainers":[{"name":"5edmat","email":"s.haroon@5edmat.com"}],"description":"Read, diff and safely apply AI coding tool configuration — Claude Code, Cursor and Zed — with atomic writes, comment-preserving edits and rollback.","keywords":["claude-code","cursor","zed","agent-skills","mcp","dotfiles","config","sync","cli","devtools"],"license":"MIT","readme":"# Agent config control plane\n\nManage AI coding tool configuration — Claude Code, Cursor, Zed — across every\ndevice you own, from one place.\n\n**Status: pre-alpha.** The engine, the CLI and the format-preserving writer are\nreal and tested — 734 tests. Writes are deliberately gated: off on Windows and\nLinux until CI verifies the path tables, and off for any tool that is not\nactually installed. See [What refuses a write](#what-refuses-a-write).\n\n---\n\n## Install\n\n```bash\nnpm install -g @5edmat/agent-sync      # or: npx @5edmat/agent-sync <command>\n```\n\nRequires Node 20.11+. Works on macOS, Linux, WSL and Windows — though writes are\ncurrently gated on Windows and Linux, see [What refuses a write](#what-refuses-a-write).\n\n```bash\nagentsync init --adopt    # capture this machine's config as your baseline\nagentsync status          # what's installed, what has drifted\nagentsync diff            # review the changes, with risk called out\nagentsync apply           # atomic, reversible; prints a rollback id\nagentsync rollback <id>   # put it back exactly\nagentsync doctor          # run this first when anything is surprising\n```\n\nEvery command takes `--json`. Exit codes mean something: `0` ok, `1` error,\n`2` usage, `3` nothing to do, `4` blocked by an unverified path.\n\nNothing is written without a plan you can read first, `apply` refuses anything\nthat can execute code unless you confirm it, and every write is backed up before\nit happens.\n\n**This release is device-local.** It reads, diffs, applies and rolls back on the\nmachine it runs on. Syncing between machines is designed but not built — see\n[What doesn't](#what-doesnt).\n\n---\n\n## Why this exists\n\nThe obvious version of this product is already dead. Worth knowing before\nreading further:\n\n- **Anthropic ships enterprise config management natively.** Managed settings\n  arrive from the claude.ai admin console at sign-in, plus MDM (`com.anthropic.claudecode`)\n  and Group Policy (`HKLM\\SOFTWARE\\Policies\\ClaudeCode`). Policy knobs already\n  exist for permissions, MCP allowlists, hooks and marketplaces. \"Fleet\n  governance for Claude Code\" is occupied by the platform owner.\n- **Skills are already cross-tool.** Cursor loads skills from `~/.agents/skills/`,\n  `~/.claude/skills/` *and* `~/.codex/skills/`. So does Zed. A user's skills\n  already work everywhere with no product involved — and there are open feature\n  requests asking to load *fewer* skills, not more.\n\nWhat is genuinely unserved:\n\n| Surface | State | Value |\n|---|---|---|\n| Skills | already cross-tool via `~/.agents/skills/` | low — solved |\n| Rules / instructions | converging on `AGENTS.md` | low-medium |\n| **Settings, permissions, sandbox** | fragmented, no convergence | **high** |\n| **MCP config** | fragmented, secret-bearing | **high** |\n| **Hooks** | fragmented, OS-specific | **high** |\n| **Cross-device sync** | unsolved everywhere | **high** |\n| **Curation** — which skills load in which tool on which device | unserved | **high** |\n\n---\n\n## Architecture\n\n```\n  web app  ── writes ──▶  DESIRED state  ──┐\n                                            ├──▶ device reconciles, reports back\n  device   ── writes ──▶  OBSERVED state ──┘\n```\n\nThe device is always the executor — it owns the filesystem. The web app only\nedits intent. Drift is `desired != observed`, which is also exactly what the\ndevice matrix renders.\n\n### Layering\n\n```\nbase ──▶ os:<os> ──▶ machine:<deviceId> ──▶ local\n```\n\n`local` never leaves the device. It's the escape hatch that makes the other\nthree safe to share. Layer placement is *enforced*, not advisory: a shell hook\ncannot be authored into `base`, because it would silently fail on every Windows\ndevice.\n\n### The adapter model\n\nEach tool implements `ToolAdapter` (`src/core/types.ts`). Two decisions carry\nmost of the weight:\n\n**`StoreLocation` is a union, not a path.** Windows managed policy is a registry\nread; macOS MDM is a plist domain; some settings arrive from a vendor server at\nsign-in. A `{ path: string }` model is wrong on day one.\n\n**`plan()` is pure; `apply()` is the only thing that writes.** The web app and\nthe device must compute byte-identical plans from identical inputs, or\n\"preview before apply\" is theatre. Plan ids are content fingerprints, and\napprovals bind to them.\n\n### Sub-file addressing\n\nA file is the wrong unit of sync. Zed puts `context_servers` (MCP) and\n`buffer_font_size` in one `settings.json` as peer keys — a per-file `syncable`\nflag has no correct value. So descriptors carry a `subtree`:\n\n```\nagent              concept=agent    syncable=true\ncontext_servers    concept=mcp      syncable=true\ntheme              concept=editor   syncable=false\nbuffer_font_size   concept=editor   syncable=false\n(remainder)        concept=other    syncable=false\n```\n\nDescriptors sharing a `fileId` **must** be coalesced by `apply()` into one\natomic read-modify-write. This also cleanly expresses Claude Code's\n`~/.claude.json`, where `mcpServers` is syncable but `oauthAccount`, `machineID`\nand `projects` are not.\n\n---\n\n## Security model\n\nThis product writes to developer machines, and those machines hold source, cloud\ncredentials, and production access. Seven controls, each with tests.\n\n### 1. The backend cannot execute code on your devices\n\nHooks are shell commands. MCP servers are `command` + `args`. `env` feeds both.\nA naive \"web app pushes config to devices\" design means whoever controls the\nbackend gets RCE on every customer's laptop.\n\nSo desired state is **signed by a key the user holds**. The backend stores and\nrelays; it cannot mint. Devices pin the key at pairing and reject anything else.\nCode-execution-class changes need explicit per-item approval carried *inside*\nthe signature — the web app collecting approvals is a convenience, the device\nnot trusting it is the control.\n\nA full backend breach degrades to denial of service, not RCE.\n\n### 2. Secrets are end-to-end encrypted\n\n```\npassphrase ──Argon2id──▶ root key ──wraps──▶ DEK ──AES-256-GCM──▶ secrets\n                                        │\n                                        ├──sealed to──▶ device X25519 pubkey\n                                        └──sealed to──▶ recovery code\n```\n\nConfig stays plaintext (you can't diff what you can't read); secret *values* are\nsealed and referenced symbolically as `${secret:github.token}`. `ServerVaultRecord`\nis written as a type so the boundary is reviewable — if a field that could carry\nplaintext appears in it, that should fail review.\n\nTwo things the UX must state plainly: losing the passphrase *and* every device\n*and* the recovery code means the secrets are gone; and revoking a device\nrotates the DEK forward but cannot un-know what that device already decrypted\n(rotate at the source too).\n\n### 3. The enumeration floor\n\n`NEVER_ENUMERATE` — `~/.ssh`, `~/.aws`, `~/.gnupg`, `.env` files, private keys,\nbrowser profiles, keychains — is enforced in **every** mode including `full`,\nand is not user-editable downward. Verified against: direct request, `..`\ntraversal, symlink escape, case variants on case-insensitive filesystems,\nnon-existent paths (fail closed, not open), and a user explicitly adding\n`~/.ssh` as a root.\n\n### 4. Secrets cannot leave the device by accident\n\n`never-sync` KeyRules are the precise defence, but they only fire when the\ndocument is the shape the rules were written for — and adapters accept more than\none. The same GitHub token was verified BLOCKED as\n`mcpServers.github.env.TOKEN` and classified **portable** as\n`stores.<id>.mcpServers.github.env.TOKEN`.\n\nAny defence that depends on the caller passing the right shape is not a defence,\nso `core/secret-guard.ts` walks whatever it is given and matches on key name and\nvalue shape. It catches tokens smuggled through `args` arrays, which path-based\nrules cannot see at all because `flatten` treats arrays as leaves. Blocked\nvalues are stripped at source and the change is still *reported* — silently\ndropping it made `diff` say \"already in the desired state\", which is safe but\nuntrue.\n\n`DesiredState` is now a tagged union so the ambiguity that caused this cannot\nrecur.\n\n### 5. One writer at a time\n\n`apply()` takes an advisory lock across verify → backup → write. Against another\nagentsync process the race is closed outright. Against a foreign writer — your\neditor, or the tool writing its own settings — a lock is powerless, so the raw\nbytes are re-fingerprinted immediately before the write. This narrows the window\nfrom \"the whole apply\" to \"between the check and one syscall\"; it cannot reach\nzero, because `rename(2)` swaps the inode and no fd we hold prevents that.\nLosing the race now aborts instead of silently winning it.\n\n### 6. Provenance gating\n\nEvery path table entry declares how it was verified:\n\n- `verified-doc` — confirmed in vendor documentation\n- `verified-fs` — confirmed against a real install\n- `inferred` — reasoned from convention\n\n**`apply()` refuses to write to `inferred` locations.** Since paths have only\nbeen filesystem-verified on macOS, writes are currently gated off on Windows and\nLinux. Being useless on an unverified platform beats corrupting it; the CI\nconformance matrix is what clears them.\n\n### 7. Detection gating\n\nProvenance answers \"do we believe this path is right?\" — not \"is this tool even\nhere?\". For a table built from vendor docs alone those come apart: a\n`verified-doc` path is writable, so on a machine with no Cursor we would happily\ncreate `~/.cursor/mcp.json`, configuring software that does not exist at a\nlocation nothing has confirmed.\n\nSo `apply()` also refuses when `detect()` reports the tool absent. This beats\ntightening provenance because it self-heals — the moment a real install exists,\nits real paths are confirmed by the same probe that gates the write. \"Installed\nbut no config yet\" is explicitly still allowed to create a first file.\n\n## What refuses a write\n\n`agentsync doctor` names whichever of these is binding:\n\n| Reason | Clears when |\n|---|---|\n| adapter cannot apply | the adapter implements it |\n| tool not installed | you install the tool |\n| path provenance `inferred` | CI confirms it on that OS |\n| managed by org policy | never — policy wins by design |\n| value looks secret | you move it to the keychain |\n\n---\n\n## Cross-platform\n\nThe all-platforms claim is only true if it's tested on all platforms.\n`.github/workflows/conformance.yml` runs the suite on macOS, Linux (with *and*\nwithout a keyring), Windows (Developer Mode on *and* off), and WSL.\n\nThe cases that matter, because fakes cannot prove them:\n\n- `rename()` over a file held by an exclusive lock, with Defender active\n- **deleting a Windows junction must not delete its target** (data-loss class)\n- headless Linux secret backend must not *hang* — every devcontainer hits this\n- `MAX_PATH` behavior vs. the long-paths probe\n\nOther cross-platform hazards handled: CRLF/LF (canonical hashing, `.gitattributes`),\ncase collisions, Windows reserved names (`CON`, `NUL`, `AUX`…), exec-bit loss on\nround trips, and symlink-vs-junction-vs-copy materialization.\n\n---\n\n## Layout\n\n```\nsrc/core/        types · reconcile · apply-engine · control-plane · vault\n                 enumeration · concepts\nsrc/adapters/    claude-code · cursor · zed\nsrc/platform/    host · atomic · links · secrets · paths · canonical\nsrc/cli/         (in progress)\nweb/             React control plane — device matrix, plan preview,\n                 layer editor, permissions editor\ndocs/            adapter-fit.md · zed-spike.md\n```\n\n---\n\n## What works today\n\n- Read, plan, and validate for Claude Code, Cursor, Zed\n- **`apply()` for Claude Code** — atomic, staleness-checked, all-or-nothing,\n  with rollback. Verified end-to-end against a real `~/.claude/settings.json`.\n- **Comment-preserving writes.** A JSONC file keeps its comments, blank lines,\n  key order and trailing commas; only the changed value spans move.\n- **A CLI**: `init`, `status`, `diff`, `apply`, `rollback`, `doctor`, `devices`\n  — meaningful exit codes, `--json` everywhere, `NO_COLOR` respected.\n- **Sub-file addressing.** Zed's `context_servers` syncs without dragging\n  `buffer_font_size`; descriptors sharing a file coalesce into one atomic write.\n- Layering with enforced portability classes and per-key merge semantics\n- Platform primitives: atomic writes with Windows retry, backup/restore,\n  symlink/junction/copy, four secret backends, path validation\n\n## What doesn't\n\n- `apply()` for Cursor and Zed — in progress\n- Writes on Windows and Linux (provenance-gated)\n- No backend exists; the web app runs against typed mocks\n- The web app predates `concept` / `subtree` / `activeWhen`, so it cannot yet\n  group rows across tools or warn that writing `.rules` disables `CLAUDE.md`\n\n---\n\n## A note on testing\n\nThree real bugs were found by writing tests for code that already \"worked\":\n\n1. **`flatten()` was blind to array-rooted documents**, so array-rooted config\n   (Zed's `tasks.json` — a list of shell commands) produced *zero changes and\n   reported success*.\n2. **Rules didn't govern their own subtrees.** `flatten()` yields leaf paths, so\n   `oauthAccount` became `oauthAccount.emailAddress`, which the rule\n   `match: 'oauthAccount'` didn't match — it fell through to the `**` catch-all\n   and account identity was classified **portable**. Every `never-sync` rule on\n   an object-valued key was silently inert.\n3. **`*Cache` lost to `**`** in rule specificity scoring, because the tiebreaker\n   rewarded consuming more of the path and `**` always consumes all of it.\n\nAll three were silent-wrong-answer bugs in the most load-bearing file in the\nproject, which had no direct tests. That was the actual gap.\n","readmeFilename":"README.md","_rev":"1-530be37d617c623df3a3926360317bfd"}