agents-vault
agents-vault [--link] [--push] [--restore] [--status][--adopt=SLUG] [--remote=URL][-v | --verbose] [-q | --quiet] [-s | --silent][-h | --help]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/<mangled-path>/memory/ and agy keeps its knowledgestore 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:
--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:
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: --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.Notes: 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.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