{"id":51317080,"url":"https://github.com/cjroth/agentsync","last_synced_at":"2026-07-01T08:31:27.051Z","repository":{"id":356188089,"uuid":"1230549090","full_name":"cjroth/agentsync","owner":"cjroth","description":"Real-time distributed agent memory.","archived":false,"fork":false,"pushed_at":"2026-05-07T02:17:55.000Z","size":113,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-07T02:35:49.192Z","etag":null,"topics":["agent-memory","agent-memory-system","agents","ai","automerge","crdts","llms"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cjroth.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-05-06T05:23:30.000Z","updated_at":"2026-05-07T02:17:59.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/cjroth/agentsync","commit_stats":null,"previous_names":["cjroth/agentsync"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/cjroth/agentsync","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cjroth%2Fagentsync","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cjroth%2Fagentsync/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cjroth%2Fagentsync/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cjroth%2Fagentsync/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cjroth","download_url":"https://codeload.github.com/cjroth/agentsync/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cjroth%2Fagentsync/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34999791,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-01T02:00:05.325Z","response_time":130,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent-memory","agent-memory-system","agents","ai","automerge","crdts","llms"],"created_at":"2026-07-01T08:31:22.446Z","updated_at":"2026-07-01T08:31:27.038Z","avatar_url":"https://github.com/cjroth.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# agentsync\n\nReal-time distributed agent memory: sync folders of markdown files between devices with point-in-time-recovery.\n\n```sh\n# Machine 1 — the hub\nagentsync init\n# Prints this device's pubkey, e.g.\n#   identity_pub  = ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...\nagentsync --listen\n# Prints \"listening on wss://0.0.0.0:443\"\n# 443 is privileged — see \"Binding 443 as a regular user\" below if you\n# get \"permission denied\", or pick another port: `agentsync --listen 0.0.0.0:8443`.\n```\n\n```sh\n# Machine 2 — a peer\nagentsync key generate\n# Generates a pubkey for this device at ~/.agentsync/id_ed25519.pub and prints it.\n# Paste it into authorized_keys on Machine 1 (or any device that already has the vault)\n# and let it sync.\n\nagentsync clone machine-1\n# Clones into a folder named after the remote vault's `name` (set on the\n# hub via `agentsync init --name \u003cname\u003e`). Pass an explicit dir if you\n# want to override: `agentsync clone machine-1 my-folder`. Bare hosts get\n# `wss://` prepended (or `ws://` with `--no-tls`); pass `wss://host:port`\n# / `ws://host:port` to be explicit.\n# Port-less URLs use the scheme default (443 for wss, 80 for ws); include\n# `:\u003cport\u003e` only if your hub binds something other than 443.\n# On first connect you'll be prompted to confirm Machine 1's identity (TOFU).\n# Pass --accept-hub-key \u003cpubkey\u003e to skip the prompt in scripts.\n```\n\nBy default only `.md` and `.markdown` files sync; edit `[sync] extensions` in\n`.agentsync/config.toml` to include other extensions.\n\n* Dead-simple `agentsync` CLI that syncs between devices\n* The CLI wraps a Rust SDK ([`agentsync-core`](./crates/agentsync-core)) that can be imported to any Rust app\n* TypeScript / WebAssembly SDK at [`@agentsync/sdk`](./sdks/typescript) with a high-level `Vault` API that mirrors the Rust SDK and runs in Node, Bun, browsers, Electron, Tauri, and IDE extensions\n* Built on Automerge which uses CRDTs to prevent merge conflicts\n* Tag snapshots to easily go back to any point in time\n* Per-device ed25519 identities; authorization via a synced `authorized_keys` file (SSH-style)\n* Zero infrastructure required\n* WSS with self-signed certs and channel-bound auth — no public CA needed\n* ssh-agent backend supported for hardware-backed keys (Secretive, 1Password, ssh-agent, YubiKey-Agent)\n* TOFU hub trust pinned per-vault in `config.toml`\n\n**Status:** alpha. See [`SPEC.md`](./SPEC.md) for the product spec and [`AUTH.md`](./AUTH.md) for the auth design.\n\n## Authentication model\n\nEach device has its own ed25519 keypair. The hub (the `--listen` peer) gates\nconnections by checking the connecting peer's pubkey against `authorized_keys`,\nan SSH-style file at the root of the synced vault that lists authorized devices:\n\n```\n# agentsync authorized_keys\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... chris-macbook\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... chris-iphone\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... homelab-nas\n```\n\n`authorized_keys` is itself synced through agentsync, so removing a device\nfrom any peer's copy disconnects them within one sync round. The line format\nis identical to `~/.ssh/authorized_keys`, so you can paste OpenSSH pubkey\noutput directly.\n\nBy default the local identity lives at `~/.agentsync/id_ed25519` (shared\nacross all of this user's vaults, like `~/.ssh/id_ed25519`). Pass\n`--identity \u003cpath\u003e` on `init` / `clone` / `key generate` to override.\n\nThe wire is encrypted via WSS with a self-signed cert that the hub\nauto-generates on first launch. Trust isn't established at the TLS layer —\nclients accept any cert — but the application-layer signature in the\nhandshake binds to the cert fingerprint, so an active MITM that re-encrypts\nto the real listener is detected and refused.\n\nTo add a new device:\n\n1. On the new device, run `agentsync key generate` (or `agentsync init` for\n   a fresh vault). Copy the printed `ssh-ed25519 ...` line.\n2. On any device that already has the vault, append it as a new line in\n   `authorized_keys`. The hub picks up the change via its file watcher\n   and logs `peer added to authorized_keys`.\n3. The new device can now connect.\n\nTo use a hardware-backed identity (Secretive, 1Password's ssh-agent,\ngpg-agent, YubiKey-Agent), point agentsync at the agent socket:\n\n```sh\nagentsync watch \\\n  --identity-agent /path/to/agent.sock \\\n  --identity-agent-pubkey \"ssh-ed25519 AAAA...\"\n```\n\nOr persist the choice in `.agentsync/config.toml`:\n\n```toml\n[identity]\nagent_socket = \"/path/to/agent.sock\"\nagent_pubkey = \"ssh-ed25519 AAAA...\"\n```\n\n## Workspace layout\n\n```\ncrates/\n  agentsync-core/     # sync engine library (compiles native + wasm32)\n  agentsync-cli/      # `agentsync` binary\n  agentsync-wasm/     # wasm-bindgen wrappers for the TypeScript SDK\nsdks/\n  typescript/         # @agentsync/sdk — npm package built from agentsync-wasm\ntests/\n  e2e/                # multi-peer end-to-end tests against the real binary\nSPEC.md               # product spec\nAUTH.md               # auth design\n```\n\nThe native engine and the wasm bindings share the exact same Automerge\nschema, frame codec, identity format, and `authorized_keys` parser — see\nthe SDK README for which surface is exposed in JS.\n\n## Build\n\n```bash\ncargo build --release\n./target/release/agentsync --version\n```\n\nRequires Rust 1.89+. To build the TypeScript SDK locally:\n\n```bash\ncd sdks/typescript\nbun install\nbun run build       # wasm-pack + tsc\nbun test            # 41 unit tests\nbun run test:e2e    # 5 e2e tests against a real `agentsync` hub\n```\n\n## TypeScript / WASM SDK\n\nThe SDK ships a high-level `Vault` API that mirrors `agentsync-core::Vault` —\nconnect, sync, watch, restore-to-time, label snapshots, file CRUD — plus all\nthe low-level primitives (Identity, Pubkey, Doc, SyncState, frame codec,\nhandshake helpers).\n\n```ts\nimport { Vault, Identity, nodeFsStorage, nodeWsTransport } from '@agentsync/sdk';\nimport WebSocket from 'ws';\n\nconst vault = await Vault.create({\n  storage: nodeFsStorage('./my-vault/.agentsync'),\n  vaultId: '6f1f1aa9-...',\n  rendezvousUrl: 'wss://hub.example.com',\n  transport: nodeWsTransport(WebSocket),\n});\n\nawait vault.writeTextFile('notes/hello.md', '# hi\\n');\nawait vault.connectWithReconnect();   // exponential backoff, runs forever\n\nvault.subscribe((e) =\u003e console.log(e.kind));\nawait vault.createLabel('before-cleanup');\nawait vault.restoreToLabel('before-cleanup');\n```\n\nFor browsers / Vite / Rollup: `import { ... } from '@agentsync/sdk/web'`. The\nbrowser bundle uses `globalThis.WebSocket` and OPFS by default. The raw `.wasm`\nis also exposed at `@agentsync/sdk/wasm` for custom loaders.\n\nSee the [SDK README](./sdks/typescript/README.md) for the full API surface,\nadapter list, and runtime targets (Node, Bun, browser, Electron, Tauri,\nObsidian, VS Code/Cursor).\n\n## Releases\n\n* **Rust crates** publish to crates.io on every push to `main` (as\n  `\u003cbase\u003e-\u003csha\u003e` pre-releases) and on `v*` tags (real semver). See\n  [`.github/workflows/publish-crates.yml`](./.github/workflows/publish-crates.yml).\n* **TypeScript SDK** publishes `@agentsync/sdk` to npm on the same\n  schedule (under the `next` dist-tag for main builds, `latest` for\n  tags). See [`.github/workflows/publish-npm.yml`](./.github/workflows/publish-npm.yml).\n* **CLI binaries** for linux x86_64/aarch64, macOS x86_64/aarch64, and\n  windows x86_64 attach to the GitHub Release on every `v*` tag. See\n  [`.github/workflows/release-binaries.yml`](./.github/workflows/release-binaries.yml).\n\n## CLI commands\n\n| Command | Description |\n| --- | --- |\n| `agentsync init [--name NAME]` | Initialize a vault. Auto-generates a `name` from the directory basename (override with `--name`). Generates an ed25519 identity (default `~/.agentsync/id_ed25519`) and seeds `authorized_keys` with it. Also adds `.agentsync/` to `.gitignore` and `.agentsignore` and writes a starter `.syncignore` (gitignore-syntax exclude list for the sync engine) — skip all three with `--no-ignore-files`. |\n| `agentsync watch [--authorized-keys KEYS]` | Watch and sync the vault at `--cwd` (default when no subcommand given). `--authorized-keys` (or the `AGENTSYNC_AUTHORIZED_KEYS` env var) merges extra `ssh-ed25519` lines into the synced `authorized_keys` on startup — handy for bootstrapping a server from a Fly.io / Railway secret. |\n| `agentsync clone \u003curl\u003e [dir] [--vault-id ID] [--accept-hub-key PK] [--no-tls]` | Clone an existing vault. The local directory defaults to the remote vault's `name` (read from the handshake); pass `dir` to override. `--vault-id` is discovered via the handshake if omitted; `--accept-hub-key` skips the interactive TOFU prompt. URLs without a scheme are taken as `wss://` (or `ws://` with `--no-tls`). Port-less URLs use the scheme default (443 for wss, 80 for ws); include `:\u003cport\u003e` only when reaching a hub bound to something other than 443. |\n| `agentsync status` | Print connection state, vault id, and local pubkey. |\n| `agentsync push` / `pull` | One-shot sync. |\n| `agentsync restore-at \u003cwhen\u003e` | Restore to a point in time. Accepts epoch ms or relative offsets like `5m`, `2h`, `1d`, `1w`. |\n| `agentsync snapshot create/list/restore/delete` | Manage named recovery points. |\n| `agentsync diff \u003cfrom\u003e [to]` | Show changes between two points in history. |\n| `agentsync compact` | Run a compaction pass. |\n| `agentsync key generate/show` | Generate this device's identity, or print its pubkey for pasting into someone else's `authorized_keys`. |\n| `agentsync hub trust \u003cpubkey\u003e` / `forget` / `show` | Manage the pinned hub identity (`[vault] hub_pubkey`). |\n| `agentsync completions \u003cshell\u003e [--install]` | Emit a shell-completion script (or `--install` to drop it in the conventional location for `bash`/`zsh`/`fish`). Supported shells: `bash`, `zsh`, `fish`, `powershell`, `elvish`. |\n\nAll subcommands operate on the vault at `--cwd` (or the `AGENTSYNC_CWD`\nenv var, falling back to the current working directory). Examples:\n\n```sh\nagentsync --cwd ~/notes status\nAGENTSYNC_CWD=~/notes agentsync push\nagentsync ~/notes        # bare-path shortcut → equivalent to `--cwd ~/notes watch`\n```\n\nThe exceptions are `clone` (takes its own destination dir) and `init`\n(creates the vault at `--cwd`).\n\n### Letting a reverse proxy terminate TLS\n\nIf you're running the hub behind a managed proxy that already terminates TLS\n(Railway, Render, Cloudflare Tunnel, an Nginx in front of you, …), pass\n`--no-tls` to skip in-process TLS:\n\n```sh\nagentsync watch --listen --no-tls       # binds 0.0.0.0:80 (plain ws)\nagentsync watch --listen 0.0.0.0:8080 --no-tls\n```\n\nPeers connect via the proxy's TLS endpoint — `agentsync clone wss://my-app.up.railway.app` —\nor directly with `agentsync clone --no-tls my-app:8080` for a plain link on a\ntrusted network.\n\nThe same toggle is exposed as the `AGENTSYNC_NO_TLS` env var (set it to\n`1` / `true` / `yes`), so on platforms like Railway you can flip it from\nthe dashboard without overriding the container start command:\n\n```sh\n# Railway (or any Docker-friendly host with edge TLS):\n#   AGENTSYNC_NO_TLS=1\n#   PORT=\u003cwhatever the platform injects\u003e      # e.g. 8080\n# The default Docker CMD picks both up automatically.\n```\n\nIn `--no-tls` mode the cert-fingerprint channel binding is degraded: the hub\nadvertises an all-zero fingerprint, so MITM detection at the TLS layer is\ndelegated to the proxy. The hub identity (TOFU-pinned per vault) is still\nverified end-to-end via the handshake signature, which a MITM cannot forge.\nUse only when you trust the network path between the proxy and the hub\n(public clouds typically run this hop on a private network).\n\n### Binding 443 as a regular user\n\n`agentsync --listen` binds `0.0.0.0:443` by default. On Unix, ports below\n1024 require elevated privileges. Pick whichever fits your setup:\n\n- **Linux** — grant the binary the bind capability once:\n\n  ```sh\n  sudo setcap cap_net_bind_service=+ep \"$(which agentsync)\"\n  ```\n\n  (Re-run after upgrades, since `setcap` xattrs don't survive a `cargo\n  install` overwrite.)\n\n- **macOS** — bind via a `LaunchDaemon` that runs as root and hands the\n  socket off, or run the hub under `sudo`. There's no `setcap`\n  equivalent.\n\n- **Docker / Fly.io / Railway** — containers default to root, so binding\n  443 inside them just works. No setup required.\n\n- **Quick alternative** — pick an unprivileged port: `agentsync --listen\n  0.0.0.0:8443`. Then peers clone with `wss://host:8443`.\n\n### Tab completion\n\n```sh\nagentsync completions bash --install   # ~/.local/share/bash-completion/completions/agentsync\nagentsync completions zsh  --install   # ~/.zfunc/_agentsync (add ~/.zfunc to $fpath)\nagentsync completions fish --install   # ~/.config/fish/completions/agentsync.fish\n```\n\nFor `powershell` / `elvish`, pipe stdout into your shell profile:\n\n```powershell\nagentsync completions powershell | Out-String | Invoke-Expression\n```\n\n`agentsync --help` for full flags.\n\n## On-disk layout\n\nagentsync state lives next to your files in `.agentsync/`:\n\n```\nmy-vault/\n├── notes/                       ← your files, plain on disk\n├── authorized_keys              ← authorized device pubkeys (synced, SSH-style)\n├── README.md\n├── .gitignore                   ← seeded by `init` to ignore .agentsync/\n├── .agentsignore                ← same, for agentsync's own ingest filter\n├── .syncignore                  ← gitignore-syntax exclude list (per-vault sync engine)\n├── .agentsync/                  ← per-vault state, managed by the CLI\n│   ├── config.toml              ← vault id, name, rendezvous url, identity path, hub_pubkey\n│   ├── doc.bin                  ← saved Automerge document (full history)\n│   ├── snapshots/index.json     ← named labels → heads\n│   └── blobs/\u003csha256\u003e           ← binary attachments\n└── .agentsync-server/           ← only on a `--listen` peer\n    ├── tls.crt                  ← self-signed cert (10-year, ed25519)\n    └── tls.key                  ← private key for the cert (mode 0600)\n\n~/.agentsync/                    ← shared across this user's vaults\n├── id_ed25519                   ← ed25519 secret seed (mode 0600)\n└── id_ed25519.pub               ← matching ssh-ed25519 pubkey\n```\n\nBack up `.agentsync/` with any tool you like (restic, borgbackup, rclone) — it\ncontains the full document history. `.agentsync-server/` only matters if this\ndevice runs `--listen`; deleting it just regenerates the cert (existing peers\nwill need to re-pin the new fingerprint).\n\n## Running as a hub (Docker / Fly.io / Railway)\n\nThe included `Dockerfile` builds a tiny image that runs `agentsync watch\n--listen` on a persisted volume. Two env vars control bootstrap:\n\n- `AGENTSYNC_VAULT_NAME` — name written into `config.toml` on first launch.\n  Used as the default local directory when peers run\n  `agentsync clone wss://your-host`.\n- `AGENTSYNC_AUTHORIZED_KEYS` — `ssh-ed25519 \u003cbase64\u003e [comment]` entries\n  separated by newlines or commas. Merged into the synced `authorized_keys` on every `watch`\n  startup; existing keys are skipped, so it's safe to leave set across\n  restarts.\n\nFly.io example (see `fly.toml`):\n\n```sh\nfly launch\nfly secrets set AGENTSYNC_AUTHORIZED_KEYS=\"$(cat ~/.agentsync/id_ed25519.pub)\"\n# After first deploy, pin the hub's identity locally:\nfly logs | grep identity_pub\nagentsync clone wss://your-app.fly.dev --accept-hub-key \"ssh-ed25519 ...\"\n```\n\n## Disaster recovery\n\n### I deleted my local clone and now files are missing on the hub\n\nIf you `rm -rf` a clone while `agentsync watch` (or an active `agentsync\nclone`) is still running, the deletions of your *data files* propagate to\nevery peer through the live sync session — there's no separate confirmation\nstep. `authorized_keys` is the one exception: a delete event for it is\ndropped at the FS-event layer, so you can't accidentally lock everyone out\nby wiping the directory. To recover the data files:\n\n1. Re-clone the vault (`agentsync clone \u003curl\u003e`). You'll get the deleted\n   state — empty or near-empty.\n2. Rewind the doc to before the deletion: `agentsync restore-at 5m` (or\n   whatever offset / epoch-ms target you need). `restore-at` is *additive*:\n   it appends new forward-going changes that re-create the files, rather\n   than rewriting history, so the recovery itself syncs back to the hub\n   cleanly.\n3. Run `agentsync watch` to push the un-delete changes up. The hub and any\n   other peers will converge on the restored state.\n\nTo avoid the situation entirely, **stop the watcher before deleting a\nclone** — Ctrl-C the running `agentsync watch` / `agentsync clone`, then\n`rm -rf`.\n\n### My hub is on Fly / Railway and the operator key got removed\n\nIf the operator's bootstrap pubkey ends up missing from `authorized_keys`\n(e.g. someone hand-edited the file or the file predates the delete guard),\nrestart the hub container. On boot, `agentsync watch` re-merges\n`AGENTSYNC_AUTHORIZED_KEYS` into the synced doc — so as long as the secret\nis still set, the bootstrap key comes back. Then re-clone locally and (if\nneeded) `agentsync restore-at` to roll back any other lost state.\n\n## Testing\n\n```bash\ncargo test --workspace            # everything (unit + e2e)\ncargo test --lib                  # unit tests only\ncargo test -p agentsync-e2e       # multi-peer end-to-end tests only\n```\n\nE2E tests spawn the real `agentsync` binary in temp directories and exercise\nsync over real WSS connections, including the four-message handshake, the\nssh-agent signing path (via an in-process mock agent), and an active-MITM\nrelay test that verifies channel binding refuses tampered connections. Per\nthe spec: if a feature isn't covered by an E2E test, it doesn't ship.\n\n## License\n\nNot yet licensed - I haven't decided yet.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcjroth%2Fagentsync","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcjroth%2Fagentsync","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcjroth%2Fagentsync/lists"}