agents-vault
Synopsis
Section titled “Synopsis”agents-vault [--link] [--push] [--restore] [--status] [--adopt=SLUG] [--remote=URL] [-v | --verbose] [-q | --quiet] [-s | --silent] [-h | --help]Description
Section titled “Description”Tracks curated agent memory in a host-scoped git repository so it survives losing a machine. Complements agents-init, which scaffolds the per-project AGENTS/ repo: that holds the shareable agent specification, while this holds the personal memory an agent accumulates.
Memory does not live in any project tree. Claude keeps it under ~/.claude/projects//memory/ and agy keeps its knowledge store under ~/.gemini/antigravity-cli/, both outside every repository.
Entries are keyed by normalized git remote URL rather than by path, so the key survives a machine change or a directory rename. The live memory directory becomes a symlink into the vault, which makes backup and restore the same operation: on a new machine, clone the vault once and the first agents-vault run in any project relinks its memory automatically. No manifest and no batch restore step are involved.
Only curated memory is tracked. Session transcripts are excluded (tens of megabytes per project, growing per session). Paths are allowlisted, never denylisted, so nothing new upstream adds can leak in. The allowlist runs all the way down, not just at the top: inside agy’s knowledge store only *.md and *.json files are copied, so a credential file or a conversation database appearing there is left behind by the same rule rather than by being known about in advance. Symlinks found inside the store are neither followed nor copied, so the allowlist bounds whose files it collects and not merely what kind.
Global state that belongs to no project is tracked as well. Claude’s global memory directory (~/.claude/memory) is symlinked into the vault exactly like per-project memory, and is only linked when one side or the other already holds something, since that path does not exist by default. agy’s knowledge store and settings.json are copied rather than symlinked: agy partitions by conversation UUID rather than by workspace, so it has no per-project slice, and its store sits beside SQLite databases whose WAL sidecars must never be live-tracked inside a git worktree. A failed copy is reported but is not fatal, because an incomplete backup still leaves the agent working.
Because the slug is derived from the remote, gaining, losing, or rewriting a project’s origin changes it. Each run detects this by reading the previous slug straight off the live memory symlink’s target (no guessing) and migrates that entry to the new slug before relinking, so memory accumulated under the old key is never orphaned. If both the old and new entries already hold content the migration is ambiguous and is refused; resolve it with --adopt=SLUG. An entry that is already at the new key but holds no memory – the shape a fresh clone always produces, since git cannot track an empty directory – is moved aside, not deleted, and its origin log is folded into the migrated entry, so a clone’s provenance survives the rename. The rename is atomic: a failure at any point leaves the vault exactly as it was and reports it.
Run with no flags, the command scaffolds the vault, syncs global state, links the current project, and commits. The other modes are exclusive and each returns as soon as it is done:
--status is a report and mutates nothing at all. It is answered before the vault is even scaffolded, so asking what the vault looks like never creates it, never copies agy state into it, and never claims ~/.claude/memory. A missing vault is reported rather than built.
--restore walks every vault entry and relinks the live memory directory of each one whose recorded origin path still exists, naming the rest so they can be rebound by hand. It is a convenience: the ordinary per- project run restores a cloned vault’s memory on its own.
--adopt=SLUG rebinds the current project’s entry to SLUG, which is how a machine-specific local-* key or an ambiguous migration is resolved. SLUG must match [a-z0-9._-]+ and be neither “.” nor “..” – the charset the slug formula itself emits – since it is interpolated into a vault path and handed to git mv. The rename and the relink are atomic: if the live memory directory cannot be repinned onto the new entry the rename is rolled back, so an ordinary run still finds the original entry.
--remote=URL points the vault at a remote; --push commits, pulls, and then pushes there. The pull happens only on this path. Committing needs no remote at all, and both wrappers run this command synchronously before starting an agent, so a fetch on the ordinary run would block every launch for as long as an unreachable remote takes to time out – and would take the local commit down with it, leaving an offline machine with no backup at all.
Arguments
Section titled “Arguments”--link |
Scaffold the vault and link this project’s memory; skip the final commit |
--push |
Commit, pull, then push to the vault remote |
--restore |
Walk the vault, relink what is possible, report the rest |
--status |
Show entries, link health, remote state, and orphans |
--adopt=SLUG |
Bind the current project to an existing vault entry |
--remote=URL |
Set the vault remote |
-v, --verbose |
Print all per-step output (default) |
-q, --quiet |
Print one summary line only if changes were made |
-s, --silent |
Suppress all output; errors only |
-h, --help |
Show this help message and exit |
Exit Status
Section titled “Exit Status”0 |
Completed successfully |
1 |
Fatal error (vault unavailable, git failure, ambiguous migration, invalid --adopt slug, nothing committed, or a push that did not reach the remote) |
Returns
Section titled “Returns”--status prints its report on stdout: the vault path, the remote and how far ahead of it the vault is, a warning for an unresolved rebase, then one line per entry reading “linked” or “orphan”, the slug, and the file count. Every other mode prints only verbosity-gated progress lines, and nothing at all when there was nothing to do.
Set __fish_agent_vault_dir to relocate the vault. Set __fish_agent_vault_autopush to 1 to also push on wrapper launch; it defaults to off because that push is synchronous and so delays every launch. With it on, the pull and the push are each capped at 20 seconds, since git has no connect timeout of its own and an unreachable remote otherwise blocks for minutes. An explicit --push is left uncapped: it is watched, and it must report what a real transfer really did. The cap is timeout(1); on a system that somehow lacks it, autopush says so on stderr and does not push at all, since an unbounded network call in front of a launch is the one outcome the cap exists to prevent. --push still works there.
Over ssh the cap is delivered by setting GIT_SSH_COMMAND, which would silently outrank the user’s own configuration – so it is not set at all when GIT_SSH_COMMAND is already exported or git’s core.sshCommand is configured. A vault remote reachable only through a particular identity file or ssh wrapper therefore keeps it, uncapped, rather than failing to authenticate for the sake of a timeout.
--adopt rebinds an entry; it does not pin its name. The slug is re-derived from the project on every run, so the next ordinary run migrates the adopted entry straight back to the canonical key, carrying the memory and the live link with it. That is the point rather than a wart: adopting is how a mismatched or ambiguous binding is repaired, not how an entry is given a permanent name of its own.
An entry’s origin file records the project path once, when the entry is created, and is never refreshed. A project that later moves on disk therefore keeps a stale path there and --restore degrades to reporting it as unplaceable rather than relinking the wrong directory. Rebind such an entry from the project itself with --adopt=SLUG.
The agy knowledge copy is merge-only. Files are copied into the vault but are never removed from it, so a fact deleted upstream from agy’s knowledge store persists in the vault indefinitely, and a restore or a fresh clone brings it back. Prune such an entry from the vault by hand if it must really be gone.
Three further variables exist only so the test suite can run against throwaway directories instead of the real home, and are not meant for everyday use. __fish_agent_vault_claude_root overrides Claude’s per-project directory (~/.claude/projects), which is where the per-project memory directories live. __fish_agent_vault_claude_home overrides Claude’s home directory (~/.claude), whose memory subdirectory holds the global memory. Those two name different paths and setting one has no effect on the other. __fish_agent_vault_agy_root overrides agy’s state directory (~/.gemini/antigravity-cli), which is only ever read from.
The last two are not optional niceties. Without them, a test run on a machine that has a real global memory directory would move it into a throwaway directory and leave a dangling symlink behind, which is strictly worse than having had no backup at all.
Example
Section titled “Example”agents-vaultagents-vault --statusagents-vault --remote=https://git.rootiest.dev/rootiest/agent-vault.gitagents-vault --pushagents-vault --adopt=git.rootiest.dev-rootiest-fish-configagents-vault --restoreDependencies: _agents_vault_dir, _agents_repo_slug, _agents_repo_local_slug, _agents_repo_ensure_symlink, _agents_repo_sync, _agents_repo_install_tools, git, hostname
Classification: self-limiting(rm,mkdir)