Loadout - VS Code extension
Working in large monorepos or across multiple repos can mean a developer has
a lot of LLM skills, prompts, and other configuration files that are specific to their
own workflow. Not wanting to commit these files to the main repo so as not to impose
them on other developers, they often end up scattered across branches or repos,
or even just in a local folder somewhere.
Loadout is a VS Code extension that makes it easy to manage and share these
developer-specific files using multiple branches of multiple separate "tooling" repos
It allows you to browse branches, toggle individual files on or off
for your current project, and consolidate a selection of files into a new branch
for sharing with others.
Loadout uses symlinks to link selected files into your project, so whichever harness
you use, extension, CLI, or other tooling, will see the files in their expected locations.
| Tick files on/off in the sidebar… |
…and they appear as symlinks in your project tree |
 |
 |
Usage
Clone your tooling repo into a gitignored loadout/ folder in your project
root:
git clone <your-tooling-repo-url> loadout/
Add .md files, skills, etc. to loadout, mirroring your project's
layout:
loadout/
├── .claude
│ └── CLAUDE.md
└── backend/.claude/skills/my-skill/SKILL.md
No tooling repo yet? Run Loadout: Initialize… (or loadout init) to
scaffold one.
Loadout auto-detects the tooling repo and project root by structure.
Use Browse branches in the sidebar to tick files on/off — that's the only way to change the selection;
files from any number of branches can be active at once. Each is read from an
on-demand git worktree (.loadout/worktrees/).
Clashes: if two selected files remap to the same destination (e.g. two
branches each have a root AGENTS.md), neither is linked. Untick one to
resolve it — see the output channel or the warning icon in Selection.
Gitignore: a file only links if its destination matches a glob in your
project's root .gitignore. If not, the sidebar offers Add to .gitignore &
retry (or pass -g/--fix-gitignore on the CLI).
Multiple skills repos
Configure loadout.repos in settings.json to link one or more tooling
repos into the same project at once, each under its own destination subpath.
repo can be a local checkout path, or a git URL (github, gitlab, bare
.git, ssh, git@host:path, ...) that loadout clones for you on first use:
"loadout.repos": [
{
"name": "backend-skills",
"repo": "/abs/path/to/backend-loadout",
"pathRules": [
{ "match": "<AGENTS>", "replace": ".claude" },
{ "match": "AGENTS\\.md$", "replace": "CLAUDE.md" },
{ "match": "^", "replace": "backend/" }
]
},
{
"name": "shared-skills",
"repo": "https://github.com/org/shared-skills.git",
"pathRules": [
{ "match": "<AGENTS>", "replace": ".claude" },
{ "match": "AGENTS\\.md$", "replace": "CLAUDE.md" },
{ "match": "^", "replace": "shared/" }
]
}
]
A URL-based repo is cloned once into a cache directory managed by the
extension; it's never fetched automatically afterward — run Loadout: Fetch
updates… from the actions menu to pull in upstream changes.
With more than one repo configured, Browse branches gains a repo level
above branches, and Selection entries are labeled repo · branch:src.
Destinations can still collide across repos (e.g. two repos with no prefix
rule in pathRules) — this is reported as a CONFLICT exactly like a
same-repo clash, naming both repos, and neither side links until resolved.
By default a configured entry's content is read from the branch root. Set
sourcePath to point at a subdirectory instead (e.g. if this repo also
keeps a repo/ convention like the zero-config case in Usage above):
{ "name": "shared-skills", "repo": "...", "sourcePath": "skills" }
Every source path's destination — renaming, prefixing, and per-skill
overrides — is controlled by pathRules: an ordered list of
regex { "match": "...", "replace": "..." } rules, applied in sequence to
the source path (each rule sees the previous rule's output). Leaving it unset
(or []) links every path unchanged — there's no built-in default rule set.
match is a plain JS regex (no delimiters, always applied globally);
replace may reference capture groups ($1, $2, ...).
If you want the <AGENTS> dir / AGENTS.md file convention (renaming to
.claude / CLAUDE.md), add it explicitly:
{
"name": "backend-skills",
"repo": "/abs/path/to/backend-loadout",
"pathRules": [
{ "match": "<AGENTS>", "replace": ".claude" },
{ "match": "AGENTS\\.md$", "replace": "CLAUDE.md" }
]
}
There's no separate "prefix" setting — landing every file from a source
under a subpath is just its own rule, and a later rule can still redirect
one specific subtree by matching (and capturing) whatever an earlier rule
produced:
"pathRules": [
{ "match": "<AGENTS>", "replace": ".claude" },
{ "match": "AGENTS\\.md$", "replace": "CLAUDE.md" },
{ "match": "^", "replace": "backend/" },
{ "match": "^backend/skills/my-skill(/.*)?$", "replace": "somewhere/else$1" }
]
For a one-off change to a single already-selected file, use Set
destination… from its context menu in the sidebar instead of editing
settings — it lets you type (or clear) a full destination path for just that
file, which is also how you resolve a clash by moving one side out of the
way.
Note: editing a source's pathRules shifts the destination of every
file it affects. Any .gitignore lines already added for those files (via
Add to .gitignore & retry, or the "N files not gitignored" row in
Browse branches) stay put pointing at the old paths — they aren't
cleaned up automatically — and the files reappear as not-yet-gitignored
under their new destination until you re-run the gitignore fix.
Leaving loadout.repos empty falls back to auto-detecting a single tooling
repo from the open workspace folders (see Usage above).
A repo that's accumulated a lot of branches you never want to see here (CI
branches, dependabot, stale feature branches) can set its own persisted
branchFilter, independent of the interactive Filter branches command
(which is a global, session-only substring filter over whatever's currently
shown, reset each time the window reloads):
{
"name": "backend-skills",
"repo": "/abs/path/to/backend-loadout",
"branchFilter": {
"include": ["^(main|release/.*)$"],
"exclude": ["-wip$"],
},
}
include/exclude are lists of plain JS regexes (no delimiters). When
include is set, only branches matching at least one of its patterns are
kept; exclude is then applied on top of that, dropping any branch matching
one of its patterns. Omit either list (or the whole branchFilter) to skip
that step — the default is no filtering.
Settings
| Setting |
Meaning |
loadout.repos |
Configure one or more skills repos — see above. Each entry: name, repo, sourcePath, pathRules, branchFilter. Leave empty to auto-detect a single repo, and the target project repo, from the open workspace folders. |
CLI
bin/loadout works standalone — no extension required. It only ever deals
with one tooling repo per invocation; loadout.repos (and thus per-repo
pathRules beyond the -a/-c convention) and the interactive "Set
destination…" override are extension-only:
loadout enable <branch> [src] add a file (or every file) to the selection, then sync
loadout disable <branch> [src] remove a file (or every file) from the selection, then sync
loadout link re-sync symlinks from the current selection
loadout list <branch> list a branch's files and enabled state
loadout clean remove all links
loadout status show selection and linked files
loadout consolidate <branch> bundle every currently-linked file into
one new branch (a fresh orphan commit);
-f overwrites an existing branch of that name
loadout init scaffold a new tooling repo
loadout help
Options: -n dry run, -f force-overwrite, -a/-c rename
<AGENTS>/AGENTS.md, -r/-w target/tooling repo paths, -j/--json,
-g/--fix-gitignore, -h/--help.
Contributing
See CONTRIBUTING.md for build, test, and packaging instructions.
Credits
Thanks to @morris7 for the first implementation of this extension and insightful feedback on the design and functionality.