Problem
graphify always writes graphify-out/ in the current directory, and the documented team workflow is to commit it to git. That breaks down quickly:
- Graphs are large — 300MB–1GB per module on big codebases; git history balloons.
- Monorepos (Linux-kernel-style, many modules) produce one
graphify-out per module — dozens of heavy folders scattered through the tree.
- Every branch overwrites the same committed folder, so branch graphs collide.
- There is no built-in way for a teammate to fetch a graph someone else already built — everyone rebuilds (API cost, time) or graphs go stale.
GRAPHIFY_OUT (env) can redirect the CLI's output, but the agent skill references the literal graphify-out/... path throughout, so an env-var redirect splits the world: CLI honours it, the skill's file reads don't.
Proposal: central graph store + repo-local sync hooks (implemented in #1752)
Opt-in via a committed .graphify/ folder. No behaviour change without it, and zero changes to the agent skill.
Commands — one group, graphify remote
graphify remote init # scaffold .graphify/ (config.json + starter push/pull hooks) — commit it
graphify remote push # run the push hook — upload the store tree
graphify remote pull # run the pull hook — download the store tree + recreate all links
graphify remote delete # leave the store — links become real local folders again
The store: links, not path redirection
.graphify/config.json — { "store": "~/graphify-store/<folder>" } — makes every graphify run keep ./graphify-out as a link (POSIX symlink; Windows directory junction, no admin rights) into:
<store>/<module-relative-path>/graphify-out
- Writes physically land outside the repo; every literal
graphify-out/... path — CLI defaults, the agent skill's code blocks, plain cat — keeps working through the link. This is why the skill stays byte-identical.
- The store path is the whole key — no
<repo> or <branch> segment. The repo points at the same store location on every branch (switch branches → no retarget, no rebuild; graphify update refreshes the graph). Each repo names its own store path in its config; remote init defaults it to ~/graphify-store/<folder>, change it to anything.
- Monorepos: one link per directory you build in, keyed by repo-relative path.
- Adoption is safe: a pre-existing real
graphify-out/ (even one tracked in git) is migrated into the store once; committing then records the graph files leaving git.
- Exit is safe:
remote delete replaces each link with a real folder holding a copy of its store data (store untouched — teammates who share it are unaffected).
- A bare
graphify-out entry is maintained in .gitignore automatically. Bare on purpose: the common graphify-out/ pattern matches only directories — git treats a symlink as a file (it would slip through), and on Windows git descends into unignored junctions. Even unignored, a symlink commits as a ~100-byte target path, never data.
The hooks: pluggable, committed in the repo
remote push/pull run a hook — any executable: Python, Node, shell, PowerShell, .cmd/.bat, or a binary. graphify core stays backend-free (no boto3/SDK dependency); teams version their sync logic next to the config. If the store folder is already shared (NFS, Dropbox, a mounted drive), no hooks are needed at all.
Resolution order:
- explicit path in
.graphify/config.json: {"push": "./scripts/push.py", "pull": "..."}
.graphify/<push|pull>.{py,js,mjs,cjs,sh,ps1,cmd,bat} committed in the repo
An executable hook runs directly (its shebang wins — e.g. #!/usr/bin/env -S uv run --with boto3 python3); otherwise graphify picks the interpreter by extension, so it also works on Windows where shebangs are ignored.
Hook environment (the whole contract — mirror GRAPHIFY_STORE_DIR, exit non-zero on failure):
| Variable |
Meaning |
GRAPHIFY_ACTION |
push or pull |
GRAPHIFY_STORE_DIR |
the configured <store> path — the tree to mirror |
GRAPHIFY_STORE |
the same store path, expanded |
GRAPHIFY_CONFIG |
path to .graphify/config.json (teams read their own extra keys) |
GRAPHIFY_REPO_ROOT |
repo top-level dir (context only — data is not here) |
GRAPHIFY_PREFIX |
the store folder's basename — a natural object-key / path prefix |
Starter templates — remote init --backend <name>
remote init scaffolds working hooks for the backend you pick (default s3); it's non-destructive (an existing config or a hook in any extension is kept). The hook is just a script — edit it freely, graphify runs whatever is there.
| backend |
write |
read |
| s3 (default) |
creds (boto3) |
creds (boto3) |
| s3-public |
creds (boto3) |
URL only — no creds, no SDK (public bucket + _manifest.json, stdlib urllib) |
| git-lfs |
GRAPHIFY_GIT_REMOTE |
same |
| rsync |
GRAPHIFY_RSYNC_DEST (SSH / mounted share) |
same |
Secrets always come from the environment (env vars, ~/.aws, ~/.ssh) — never the repo. Non-secret keys you add to config.json (e.g. the public store_url) are yours to read via GRAPHIFY_CONFIG.
Asymmetric access (s3-public) is the "sometimes you don't need secure" case: make the bucket public-read, commit its public store_url in config.json, and readers pull over plain HTTP with a manifest — only pushers ever touch credentials. Verified live end-to-end on a real public bucket: a fresh clone with credentials stripped from the environment pulled every module by URL alone.
Team flow
# publisher (once)
graphify remote init && $EDITOR .graphify/config.json && git add .graphify && git commit
graphify . # writes through the link into the store
graphify remote push
# teammate
git clone … && cd …
graphify remote pull # downloads the store tree AND recreates every module's link
graphify query "how does auth work?"
remote pull fans a link out for every module found in the store, so one pull on a fresh clone recreates the whole working set — root-level reads like merge-graphs ./services/api/graphify-out/graph.json work immediately.
Why hooks instead of built-in adapters
Backends differ per team (S3, MinIO, GCS, rsync, LFS, network shares) and each would drag a heavy optional dependency into core. A one-line contract keeps graphify lean and the sync logic reviewable in the repo.
Why links instead of path redirection
The skill and users' habits use the literal graphify-out/... path everywhere (75 references in skill.md alone). A link redirects the bytes while every existing path keeps working — the filesystem does the redirection, so the CLI and the skill can never disagree. Skill, skillgen fragments, and generated artifacts remain byte-identical to v8.
Problem
graphify always writes
graphify-out/in the current directory, and the documented team workflow is to commit it to git. That breaks down quickly:graphify-outper module — dozens of heavy folders scattered through the tree.GRAPHIFY_OUT(env) can redirect the CLI's output, but the agent skill references the literalgraphify-out/...path throughout, so an env-var redirect splits the world: CLI honours it, the skill's file reads don't.Proposal: central graph store + repo-local sync hooks (implemented in #1752)
Opt-in via a committed
.graphify/folder. No behaviour change without it, and zero changes to the agent skill.Commands — one group,
graphify remoteThe store: links, not path redirection
.graphify/config.json—{ "store": "~/graphify-store/<folder>" }— makes every graphify run keep./graphify-outas a link (POSIX symlink; Windows directory junction, no admin rights) into:graphify-out/...path — CLI defaults, the agent skill's code blocks, plaincat— keeps working through the link. This is why the skill stays byte-identical.<repo>or<branch>segment. The repo points at the same store location on every branch (switch branches → no retarget, no rebuild;graphify updaterefreshes the graph). Each repo names its own store path in its config;remote initdefaults it to~/graphify-store/<folder>, change it to anything.graphify-out/(even one tracked in git) is migrated into the store once; committing then records the graph files leaving git.remote deletereplaces each link with a real folder holding a copy of its store data (store untouched — teammates who share it are unaffected).graphify-outentry is maintained in.gitignoreautomatically. Bare on purpose: the commongraphify-out/pattern matches only directories — git treats a symlink as a file (it would slip through), and on Windows git descends into unignored junctions. Even unignored, a symlink commits as a ~100-byte target path, never data.The hooks: pluggable, committed in the repo
remote push/pullrun a hook — any executable: Python, Node, shell, PowerShell,.cmd/.bat, or a binary. graphify core stays backend-free (no boto3/SDK dependency); teams version their sync logic next to the config. If the store folder is already shared (NFS, Dropbox, a mounted drive), no hooks are needed at all.Resolution order:
.graphify/config.json:{"push": "./scripts/push.py", "pull": "..."}.graphify/<push|pull>.{py,js,mjs,cjs,sh,ps1,cmd,bat}committed in the repoAn executable hook runs directly (its shebang wins — e.g.
#!/usr/bin/env -S uv run --with boto3 python3); otherwise graphify picks the interpreter by extension, so it also works on Windows where shebangs are ignored.Hook environment (the whole contract — mirror
GRAPHIFY_STORE_DIR, exit non-zero on failure):GRAPHIFY_ACTIONpushorpullGRAPHIFY_STORE_DIR<store>path — the tree to mirrorGRAPHIFY_STOREGRAPHIFY_CONFIG.graphify/config.json(teams read their own extra keys)GRAPHIFY_REPO_ROOTGRAPHIFY_PREFIXStarter templates —
remote init --backend <name>remote initscaffolds working hooks for the backend you pick (default s3); it's non-destructive (an existing config or a hook in any extension is kept). The hook is just a script — edit it freely, graphify runs whatever is there._manifest.json, stdliburllib)GRAPHIFY_GIT_REMOTEGRAPHIFY_RSYNC_DEST(SSH / mounted share)Secrets always come from the environment (env vars,
~/.aws,~/.ssh) — never the repo. Non-secret keys you add toconfig.json(e.g. the publicstore_url) are yours to read viaGRAPHIFY_CONFIG.Asymmetric access (
s3-public) is the "sometimes you don't need secure" case: make the bucket public-read, commit its publicstore_urlinconfig.json, and readers pull over plain HTTP with a manifest — only pushers ever touch credentials. Verified live end-to-end on a real public bucket: a fresh clone with credentials stripped from the environment pulled every module by URL alone.Team flow
remote pullfans a link out for every module found in the store, so one pull on a fresh clone recreates the whole working set — root-level reads likemerge-graphs ./services/api/graphify-out/graph.jsonwork immediately.Why hooks instead of built-in adapters
Backends differ per team (S3, MinIO, GCS, rsync, LFS, network shares) and each would drag a heavy optional dependency into core. A one-line contract keeps graphify lean and the sync logic reviewable in the repo.
Why links instead of path redirection
The skill and users' habits use the literal
graphify-out/...path everywhere (75 references inskill.mdalone). A link redirects the bytes while every existing path keeps working — the filesystem does the redirection, so the CLI and the skill can never disagree. Skill, skillgen fragments, and generated artifacts remain byte-identical tov8.