agents-vault
Synopsis
Section titled “Synopsis”agents-vault [--link] [--push] [--push-bg] [--restore] [--status] [--dismiss-hint] [--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/<mangled-path>/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.
A project’s devlogs are tracked too. AGENTS/devlogs/ holds handoff documents and, beside them, the backups a job saved before changing a live system; the AGENTS/ sub-repo has no remote, so without this they last as long as the machine. They are copied into the project’s entry under agents/devlogs/ and committed with the memory. Unlike agy’s store this copy is bounded by location, not by file type: backups are zone dumps, client lists and original scripts, and an extension rule would drop exactly those. Symlinks are not followed, dot-led names are skipped, and a file over 1 MiB is left behind so a stray database dump cannot bloat a history that keeps every version.
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.
The vault is private: its root directory is created with mode 700 whatever the umask, since it holds personal agent memory and config. A vault that is laxer than that (made by an earlier version, or by a manual clone) is tightened on the next run, silently, together with the regular files directly in the root; the files it writes there itself (.version, .gitignore, README.md) are created 600. The git object store and the entries below the root are not touched; the 700 root is what keeps them out of reach.
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.
--push-bg is what the claude and agy wrappers pass. After the ordinary commit it starts a detached worker that pushes the vault to its remote and returns immediately; the launch never waits on the network. It is started only when something is actually unpushed, the worker is bounded to 60 seconds, never prompts, never runs twice at once, and backs off for ten minutes after a failure. It only pushes: it does not pull or rebase, so a remote that has moved on makes it fail and --push is the way to reconcile. The outcome of the last attempt is kept in .git/agent-vault-bgpush.state and shown by --status, which is how a failure nobody was watching gets noticed. Set __fish_agent_vault_bgpush to 0 to switch it off.
A vault with no remote prints a hint once per interactive shell session (from a verbose or --quiet run, so the claude wrapper shows it) saying how to add one. It stops when a remote is set or after --dismiss-hint, and never appears in scripts or with --silent.
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 |
--push-bg |
Commit, then push from a bounded background worker |
--restore |
Walk the vault, relink what is possible, report the rest |
--status |
Show entries, link health, remote state, and orphans |
--dismiss-hint |
Stop the no-remote hint (sets a universal variable) |
--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, nothing committed, or a push that did not reach the remote) |
2 |
Unknown option, --remote without a URL, or an invalid --adopt slug |
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 a mirror: a note deleted from agy’s knowledge store is removed from the vault’s global/agy/knowledge on the next run, so a restore or a fresh clone does not bring it back. Only that one directory is ever pruned. Nothing is removed when the store is missing, unreadable, or holds nothing the vault has a counterpart for (an unmounted or emptied store is not a deletion), nor after a copy that failed in part; deleting the very last note upstream therefore needs a by-hand removal from the vault. Earlier commits keep the deleted file in the vault’s history.
The vault’s .gitignore is created when absent and line-merged when it exists: each rule the vault relies on (*.db and its two sidecars, and the /.adopt-stash and /.migrate-stash fallbacks) that is not already there as a whole line is appended in one commented block. Lines of your own are never reordered or removed, and a file that is already complete is not touched, so a vault scaffolded by an earlier version gains a rule added later without any manual step.
The devlog copy, unlike the knowledge mirror, is merge-only, and it only ever goes one way. A log deleted or moved in AGENTS/devlogs/ stays in the vault, and an ordinary run never copies back out, because that would resurrect a file removed on purpose. To bring devlogs back on a new machine, copy the entry’s agents/devlogs/ over AGENTS/devlogs/ without overwriting (cp -rn). A file over 1 MiB is skipped, and only a verbose run says so. The vault’s *.db ignore still applies, so a SQLite file in a backups folder is never committed. Nothing is scanned for secrets: the devlog rules (no secret values, scanned backups) are what keep them out, and the copy trusts them.
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.
This header covers usage only. The full concept/behavior/purpose write-up – how this relates to the per-project AGENTS/ repository agents-init manages, and where each kind of agent state actually lives – is in docs/manual/17-agent-tooling.md. Update that section in the same change whenever this function’s behavior changes; see “Dedicated manual sections for complex subsystems” in CONTRIBUTING.md.
Example
Section titled “Example”agents-vaultagents-vault --statusagents-vault --remote=https://git.rootiest.dev/rootiest/agent-vault.gitagents-vault --pushagents-vault --quiet --push-bgagents-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, _agents_vault_bgpush, _agents_vault_prune_agy, _private_dir, _agents_vault_ensure_gitignore, git, hostname
Classification: self-limiting(rm,mkdir), manual-section(17-agent-tooling)
See also: 17. AI AGENT TOOLING