vendy-cli
vendy
Vendor external repositories, or subdirectories of them, as read-only, agent-ready reference material.
npx vendy-cli add https://github.com/Effect-TS/effect
That's the Effect repository snapshotted at .vendy/effect, pinned to an exact commit, recorded in vendy.json, and committed. And unlike a hand-rolled subtree, it stays managed afterwards: an inventory, drift status, safe updates, and clean removal, one command each.
Why
Coding agents are substantially more effective when they can explore real source code instead of relying on documentation or web search — docs explain what an API does, but agents learn from how it's actually used. The Effect team's post The One Weird Git Trick That Makes Coding Agents More Effect-ive makes the case and recommends vendoring dependency repositories with git subtree.
Subtree does this job fine — git subtree add --squash vendors a repo and git subtree pull updates it. But git remembers none of it for you: every update means re-typing the prefix, URL, and branch per repo, there's no inventory of what's vendored at which version, no way to ask whether anything is behind upstream, and no way to take just part of a repository that's too big to take whole. vendy is that same workflow with the bookkeeping made first-class: a manifest, exact pins, drift status, and one-command updates. Credit where due — the idea, the editor excludes, and the agent conventions vendy prints all come from that post; vendy's contribution is giving them a lifecycle.
To be clear about what this is: an agent-context tool, not a general-purpose vendoring manager. Vendored code is read-only reference material. It is never edited locally and never pushed back upstream.
Quickstart
You need Node ≥ 20 and git on your PATH. There is nothing to install — run vendy with npx vendy-cli (or pnpm dlx vendy-cli). Or let your agent run it for you: install the vendy agent skill with the skills CLI, and vendoring becomes something you ask for — the agent runs the commands, applies the editor and agent setup below, and gives each vendored repo an AGENTS.md line saying when to reach for it.
npx skills add https://github.com/Zebeqo/vendy --skill vendy
Vendor a repository by pasting its GitHub URL:
$ npx vendy-cli add https://github.com/Effect-TS/effect
resolving default branch from https://github.com/Effect-TS/effect.git
fetching 46997fa
materializing → .vendy/effect
vendored effect@46997fa → .vendy/effect
committed cb6d771: vendy: add effect@46997fa
First repository vendored — finish your editor and agent setup:
Merge into .vscode/settings.json:
…
Three things just happened:
- The snapshot landed at
.vendy/effect— plain files, no upstream git machinery. vendy.jsonwas created at your repo root recording the entry and its exact commit pin.- One commit was made —
vendy: add effect@46997fa— containing the snapshot, the manifest, and a maintained.gitattributesblock. Nothing else of yours was touched or staged.
Because this was your first entry, vendy also printed two ready-to-paste snippets: .vscode/settings.json excludes so .vendy/ stays out of search, file watching, and auto-imports, and an AGENTS.md section telling your agent to treat .vendy/ as read-only reference material — both adapted almost verbatim from the blog post's recommended setup. Paste them once and you're set up — vendy prints rather than writes because those files are yours, not vendy's.
The full editor and agent setup, in case you missed the printed output
This is a copy of what vendy prints after the first add; the CLI output is the canonical version.
Merge into .vscode/settings.json:
{
"search.exclude": {
".vendy/**": true
},
"files.watcherExclude": {
".vendy/**": true
},
"js/ts.preferences.autoImportFileExcludePatterns": [".vendy/**"]
}
Add to AGENTS.md (or your agent's instructions file):
## Vendored Repositories
This project vendors external repositories under `.vendy/`.
- Use vendored repositories as read-only reference material when working with related libraries.
- Prefer examples and patterns from the vendored source code over generated guesses or web search results.
- Do not edit files under `.vendy/` unless explicitly asked.
- Do not import from `.vendy/`; application code must continue importing from normal package dependencies.
The printed output ends with one more line — a pointer to the agent skill install that opens this quickstart.
That's zero to vendored. Your agent can now explore .vendy/effect/ like any other part of the project.
Commands
Flags shown here are the ones you'll reach for; npx vendy-cli <command> --help is the complete reference.
vendy add
npx vendy-cli add <url> [paths...] [--ref <ref>] [--name <name>] [--temp]
Vendor a repository: URL in, pinned snapshot at .vendy/<name>, manifest entry, one commit. --ref pins a branch, tag, or commit SHA (default: the remote's default branch). --name overrides the entry name.
npx vendy-cli add https://github.com/Effect-TS/effect.git # default branch
npx vendy-cli add https://github.com/Effect-TS/effect.git --ref effect@3.19.9 # tag-pinned
Paste a GitHub URL
The fastest way to invoke add is to not construct the command at all — copy the URL from your browser, paste it:
# Repository page → that repo, default branch
npx vendy-cli add https://github.com/Effect-TS/effect
# Commit page → a SHA pin that `update` will never silently move
npx vendy-cli add https://github.com/Effect-TS/effect/commit/46997fa60401f5e3c93daa4b61f7df8e31caaab4
(Directory pages work too — see the large-repo section below.) This is pure input sugar: the URL is lowered to clone URL + --ref before anything runs, so the fetch itself stays host-agnostic — any git URL from GitLab, self-hosted instances, or private remotes works everywhere, using your existing git credentials. And a github.com URL that isn't a recognized shape (a file page, a release page) is never silently misread — vendy errors and tells you the corrected command.
Vendoring part of a large repo
Some repositories are too big to take whole — nobody wants all of Electron in their working tree. When taking everything is off the table, vendor the part that's useful as context: one package's source out of a giant monorepo, or just the docs. Pass paths (or paste a GitHub directory URL) to vendor only those directories:
$ npx vendy-cli add https://github.com/electron/electron/tree/main/docs
resolving https://github.com/electron/electron/tree/main/docs
fetching 349e1a6 (docs)
materializing → .vendy/electron
vendored electron@349e1a6 (docs) → .vendy/electron
committed acc0dd0: vendy: add electron@349e1a6 (docs)
Seconds, not the minutes a hundred-thousand-commit clone would take. The equivalent spelled out is npx vendy-cli add https://github.com/electron/electron docs --ref main. Upstream's repo-relative layout is preserved (.vendy/electron/docs/…) so internal cross-references keep resolving, all of an entry's paths share one pin so they can never drift to different commits, and upstream's root-level LICENSE/COPYING/NOTICE files always ride along at the snapshot root — redistributing a subset without its license is legally sloppy, so vendy is correct by default.
A whole repo is still the normal case — when upstream is small enough to take whole, take it whole.
--temp: vendor without committing
Sometimes a snapshot shouldn't land in git — you're evaluating a library you may not keep, answering a one-off question, or you'd rather keep the reference out of the project's history. --temp vendors a snapshot that never touches git: the entry is recorded locally to your clone and worktree instead of in vendy.json, and git status is byte-identical before and after.
$ npx vendy-cli add --temp https://github.com/fuma-nama/fumapress/tree/dev/apps/docs/content/docs
…
vendored fumapress@667086e (apps/docs/content/docs) → .vendy/fumapress
temp: vendy: add fumapress@667086e (apps/docs/content/docs)
Temp entries are first-class: list and status show them with a temp marker, update refreshes them, and remove deletes them. Once the entry has served its purpose it has exactly two exit paths — npx vendy-cli remove <name> if it didn't earn its place, npx vendy-cli promote <name> if it did. Choose by visibility, not task size: a reference you'll want again when the code is revisited belongs committed — even for a small feature — so its AGENTS.md line can point future work back at it.
vendy promote
npx vendy-cli promote <name>
Commit this worktree's temp entry in place. Offline and refetch-free: vendy hash-verifies the on-disk snapshot against the recorded content hash, moves the entry from the local manifest into vendy.json, and commits the already-materialized files — so what lands in history is exactly the bytes you evaluated during the task, not whatever upstream's branch tip drifted to since.
$ npx vendy-cli promote fumapress
promoted fumapress@667086e (apps/docs/content/docs) → .vendy/fumapress
committed eb463ce: vendy: add fumapress@667086e (apps/docs/content/docs)
If the snapshot was locally modified, promotion is refused — vendored code is read-only, and there is deliberately no --force; revert the edits or remove and re-add.
vendy status
npx vendy-cli status [--log]
Two checks per entry: is the pin behind the tracked ref upstream (network), and does the snapshot still match its recorded content hash (offline — catches anyone editing reference material).
$ npx vendy-cli status
effect main @ d24511f behind (tip 46997fa) clean
--log answers the question behind raises — behind by what? Under each behind entry, the upstream commit subjects you're missing (filtered to your vendored paths, for subset entries):
$ npx vendy-cli status --log
effect main @ d24511f behind (tip 46997fa) clean
46997fa Fix Graph shortest-path edge weight validation (#6426)
b330264 Fix child process force-kill escalation (#6411)
a79f5ec fix(vitest): preserve schema arbitraries in records (#6416)
a328835 Fix Graph.bellmanFord self cycle detection (#6425)
…
Preview with status --log, decide, then run update — the update is never blind.
vendy update
npx vendy-cli update [name...] [--ref <ref>]
Update branch-tracking entries to the latest upstream commit; no names means all of them. Tag- and SHA-pinned entries are reported pinned and left untouched — blanket npx vendy-cli update is always safe to run and never moves a deliberate pin.
$ npx vendy-cli update
effect: resolving main
effect: fetching d24511f → 46997fa
effect: materializing → .vendy/effect
updated effect d24511f → 46997fa
committed 4d0f993: vendy: update effect d24511f → 46997fa
Each moved entry is its own commit. Moving a pin — or retracking to a different branch — requires explicit intent: npx vendy-cli update <name> --ref <new> on exactly one entry.
vendy list
npx vendy-cli list
The inventory, straight from the manifest — works offline. Every snapshot lives at .vendy/<name>, so destinations aren't echoed per row.
$ npx vendy-cli list
effect https://github.com/Effect-TS/effect.git main @ 46997fa
fumapress https://github.com/fuma-nama/fumapress.git dev @ 667086e (apps/docs/content/docs) temp
vendy remove
npx vendy-cli remove <name>
Delete the snapshot and its manifest entry in one commit (or, for a temp entry, with no git activity at all). Removing the last committed entry also removes vendy's .gitattributes block — add followed by remove leaves the working tree byte-identical.
$ npx vendy-cli remove fumapress
removed fumapress (.vendy/fumapress)
committed 70891e0: vendy: remove fumapress
vendy.json
One manifest at the repo root, created by the first add:
{
"repos": [
{
"name": "electron",
"url": "https://github.com/electron/electron.git",
"ref": "main",
"commit": "349e1a6c9f508f3e5a68a44831bb0f1cd722fd66",
"hash": "sha256:4f9c83a82a92e05f8ef96b915133fd7bdb664e496e37b7ed0aff1feb8131ef28",
"paths": ["docs"]
}
]
}
vendy maintains this file — read it, review it in diffs, but change entries through the verbs (update --ref, remove, add), not a text editor: there is no reconcile step that would apply a hand edit.
name— entry name; the snapshot lives at.vendy/<name>.url— clone URL, normalized (pasted GitHub URLs are canonicalized to the.gitspelling).ref— what the entry tracks: a branch (follows the tip viaupdate), or a tag or commit SHA (a pin thatupdatereports and skips).commit— tool-written: the exact SHA the current snapshot was taken from.hash— tool-written: content hash of the snapshot, for offline local-modification detection.paths— optional: subdirectories vendored (see the large-repo section). Absent means the whole repo. One entry per upstream repo, so all its paths share one pin.
There is no lockfile — the resolved pin is committed together with the snapshot bytes it describes, so they cannot drift apart.
How it works
Every vendor operation is a snapshot, not a subtree. vendy shells out to your system git for a shallow, blob-filtered clone with a sparse checkout of only the wanted paths, resolves the ref to an exact commit SHA, and copies the files into .vendy/<name>. No upstream git machinery — no submodule pointers, no subtree merge history — is retained; the result is plain files in your repository. Whole-repo and subset vendoring are the same code path, which is why grabbing docs/ out of a repository the size of Electron takes seconds.
Git behavior is deliberately boring. Each mutation produces one commit with a structured message — vendy: add effect@46997fa, vendy: update effect d24511f → 46997fa — and vendy stages only the paths it owns (the manifest, the entry's snapshot, its .gitattributes block). Unrelated work-in-progress elsewhere in your tree never blocks a vendy command and is never swept into a vendy commit. Git hooks run normally; every mutating verb accepts --no-verify to skip them, and --no-commit to stage instead of committing.
Housekeeping is automatic. vendy maintains a marker-delimited block in .gitattributes marking .vendy/** as linguist-vendored, so vendored code stays out of your language statistics and collapses in diffs; the block appears with the first entry and disappears with the last. And license files always travel with subset snapshots, as described in the large-repo section.
Non-goals
- Editing vendored code — it's read-only reference material, and
statuswill call out local modifications. - Pushing changes back upstream, or any patch management.
- Interop with existing
git subtreeorgit submodulemetadata. - Host-specific transport APIs — pasted GitHub URLs are client-side syntax sugar; fetching is plain git and works with any host your credentials can reach.
License
MIT