Observed since 21 Oct, 09:22 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
Fetch and deploy AI agent resources from git repos to local tool directories
A package manager for AI coding tools. Declare skills, commands, agents, MCP servers, hooks, and any other resource your editor or CLI consumes in one agpack.yml. Run agpack sync and agpack fetches them from git and deploys them where each tool expects — Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, OpenCode, Windsurf, Google Antigravity, and any custom tool you describe in a YAML manifest.
Every AI coding tool has its own conventions: .claude/skills/, .cursor/commands/, .codex/agents/, .github/prompts/. MCP server configs live in subtly different shapes across tools (mcpServers vs mcp_servers vs mcp vs servers). Sharing the same resources across tools — and updating them across projects — turns into manual copy-paste with quiet drift.
agpack replaces that with a single declarative file. It owns the keys it writes, leaves everything else alone, and remembers exactly what it did so removing a dependency cleanly rolls back.
pipx install agpack # or: uv tool install agpack
Requires Python 3.11+ and git on PATH.
agpack init # creates agpack.yml with commented examples
A minimal agpack.yml:
targets:
- claude
- opencode
dependencies:
skills:
- url: https://github.com/owner/repo
path: skills/my-skill
# MCP server entries deploy as patches into each target's config file.
# ${bucket} is supplied by the target manifest, so the same patch
# writes mcpServers.filesystem on Claude and mcp.filesystem on OpenCode.
mcp:
- key: ${bucket}.filesystem
value:
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "."]
agpack sync
Skills land in .claude/skills/my-skill/ and .opencode/skills/my-skill/. The MCP server is added to .mcp.json and opencode.json. Run agpack sync again after editing — removed entries are cleaned up automatically and the lockfile remembers what to restore.
Every resource agpack deploys falls into one of three kinds. Once you know the kinds, the rest of the tool is just "declare resources of these kinds; declare which tools (targets) get them."
| Kind | What it does | What you write in agpack.yml |
|---|---|---|
copy-directory | Copy a directory tree from a fetched git repo into <path>/<name>/. A folder-of-folders expands to one bundle each. | { url, path?, ref? } |
copy-file | Copy individual files from a fetched git repo into <path>/<name>. A folder-of-files expands to one item per file. | { url, path?, ref? } |
edit-file | Read a JSON or TOML config, apply patches, write it back. Only touches keys agpack owns; everything else is preserved. | { key, value, strategy? } |
A target is a YAML manifest that maps resource type names (skills, commands, mcp, anything you like) to a kind + destination path. agpack ships built-in manifests for the eight common tools and lets you override them or add your own.
dependencies: is keyed by resource type name. The value is a list of entries. The shape of each entry depends on the kind the target uses for that resource type:
{ url, path?, ref? }.{ key, value, strategy? }.dependencies:
skills: # copy-directory on every built-in
- url: https://github.com/owner/skills-repo
path: skills/code-review
ref: v1.2.0 # tag, branch, or commit SHA
commands: # copy-file
- url: https://github.com/owner/cmds
path: commands/review.md
agents: # copy-file
- url: https://github.com/owner/agents
path: agents/backend-expert.md
mcp: # edit-file
- key: ${bucket}.filesystem
value:
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "."]
Any entry — fetch or patch — can carry a target: list to restrict it to a subset of the configured targets:, instead of every target that declares the resource type:
dependencies:
skills:
- url: https://github.com/owner/claude-only-skill
target: [claude] # only deployed to claude, even if opencode is also configured
Leaving target: off (the default) means every target that declares the resource type, exactly like before this existed. An unknown name in target: fails agpack sync/agpack status immediately, same as a typo in the top-level targets: list. See Patches below for the main reason this exists — one dependency entry, one native value shape per tool.
url takes any string git clone accepts — HTTPS, SSH, local paths. Auth goes through your system git config (SSH keys, credential helpers, etc.).
url can also be a list of fallback URLs, tried in order:
- url:
- git@github.com:owner/repo.git # SSH for team members with keys
- https://github.com/owner/repo # HTTPS fallback
path: skills/my-skill
ref: v1.2.0
path can also be a list, to pull several paths from the same repo without repeating the url block — each expands into its own dependency sharing the same url/ref (note the asymmetry: a url list means fallbacks for one source, a path list means multiple sources):
- url: https://github.com/owner/skills-repo
path:
- skills/code-review
- skills/productivity/handoff
path can point at a single file, a single folder, or a parent directory containing multiple items. agpack figures out what's inside:
path: points at… | What deploys |
|---|---|
| One skill folder (with files) | One skill bundle named after the folder |
| A folder of skill subfolders | One bundle per subfolder |
| One command/agent file | One file |
| A folder of command/agent files | Every non-hidden file |
| A folder of subfolders | Files collected from each subfolder |
Sync fails with an explicit error if a folder contains nothing deployable.
edit-file)A patch is a { key, value, strategy } triple. key is a dotted path into the destination config file. value is whatever Python value the consuming tool expects to find there (a dict for an MCP server entry, a string for a permission, a dict for a hook entry — agpack is schema-agnostic). strategy is replace (the default — overwrites whatever's at the path) or append (treats the path as a list and adds one element).
dependencies:
mcp: # strategy defaults to replace
- key: ${bucket}.filesystem
value:
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "."]
env:
API_KEY: ${API_KEY}
hooks: # an append patch — hooks live in a list
- key: ${bucket}.PreToolUse
strategy: append
value:
matcher: "Write|Edit"
hooks:
- type: command
command: "$${CLAUDE_PROJECT_DIR}/lint.sh"
permissions:
- key: ${bucket}.allow
strategy: append
value: "Read(/etc/**)"
${bucket} handles where a patch lands (which key each target's manifest uses), but some tools' MCP schemas diverge in shape, not just key name — OpenCode, for one, wants a single command array, type: local, and enabled: true where Claude/Cursor/Codex want separate command/args and type: stdio. target: lets two patches share a key and each own their tool's native shape:
dependencies:
mcp:
- key: ${bucket}.context7
target: [claude, cursor, codex]
value:
type: stdio
command: npx
args: ["-y", "@upstash/context7-mcp@latest"]
- key: ${bucket}.context7 # same key, disjoint target scope — no collision
target: [opencode]
value:
type: local
command: ["npx", "-y", "@upstash/context7-mcp@latest"]
enabled: true
A few things to know:
${bucket} is a per-target variable supplied by each target manifest. Claude's mcp resource ships bucket: mcpServers, Codex's ships bucket: mcp_servers, OpenCode's ships bucket: mcp. One patch deploys correctly to all three.$${X} writes a literal ${X} to the destination file. Useful when the consuming tool resolves variables at runtime — Claude Code itself expands ${CLAUDE_PROJECT_DIR} when a hook fires.\. inside a segment to embed a literal dot. mcpServers.example\.com writes to mcpServers["example.com"].append requires the path to resolve to a list (created empty if absent).replace restores the value that was there before agpack overwrote it (or deletes the key if agpack created it); append removes the exact item agpack added by deep-equality. agpack never deletes content it didn't write.Use ${name} in any string. The lookup table merges two sources, target wins on collision:
edit-file resource's vars: block. Per-target, only visible inside patches targeting that resource..env, then global .env, then shell environment. Available in dependency URLs/paths/refs and recursively in patch keys and values.dependencies:
skills:
- url: https://github.com/${GITHUB_ORG}/shared-skills
path: skills/my-skill
mcp:
- key: ${bucket}.context7 # ${bucket} from the target manifest
value:
command: npx
args: ["-y", "@context7/mcp-server"]
env:
CONTEXT7_API_KEY: ${CONTEXT7_API_KEY} # from env
$$ writes a literal $ (so $${X} becomes ${X} in the file — pass runtime variables through to the consuming tool). Missing ${name} references error at apply time, naming the variable and the patch context.
agpack targets list # show every available target
agpack targets show claude # print Claude's manifest as YAML
agpack ships manifests for nine tools: claude, codex, copilot, cursor, gemini, opencode, pi, windsurf, antigravity. The manifests themselves are the source of truth — browse them at agpack/builtin_targets/ or introspect locally:
agpack targets list # every target + its resource types
agpack targets show claude # the full manifest as YAML
One thing worth knowing that isn't obvious from the file names:
~/.codeium/windsurf/mcp_config.json and ~/.gemini/antigravity/mcp_config.json), which agpack does not manage.skills (.pi/skills) and commands — which map to pi's prompt templates in .pi/prompts.Add a target_definitions: block to override a built-in or add a new tool:
targets:
- claude
- my-internal-tool # custom target, defined below
target_definitions:
# Override the built-in claude target — full replacement, no deep merge.
claude:
skills:
kind: copy-directory
path: .my-claude/skills
commands:
kind: copy-file
path: .my-claude/commands
# Brand-new target. Declare any resource type names you want; the same
# names must appear under `dependencies:` to be deployed.
my-internal-tool:
skills:
kind: copy-directory
path: .myaitool/skills
rules:
kind: copy-file
path: .myaitool/rules
settings:
kind: edit-file
path: .myaitool/settings.json # format inferred from .json/.toml
vars:
bucket: mcpServers
Precedence (highest first): project target_definitions: → global target_definitions: → bundled built-in. When a name appears in target_definitions:, that entry fully replaces the built-in; agpack does not deep-merge.
Tip: agpack targets show <name> prints the resolved manifest as YAML — copy-paste it into target_definitions: as a starting point.
skills, commands, agents, mcp, hooks are not reserved — they're just the names the built-in target manifests use. Your custom targets can declare any name (rules, prompts, personas, lints, examples). agpack only matches names between dependencies: and target resource blocks; if the same name appears in multiple targets they must agree on kind:.
A global config shares dependencies across every project on your machine — skills, agents, or MCP servers you want everywhere.
agpack init --global # creates ~/.config/agpack/agpack.yml
# ~/.config/agpack/agpack.yml — same shape, no `targets:` (those stay per-project)
dependencies:
skills:
- url: https://github.com/owner/shared-skills
path: skills/code-review
mcp:
- key: ${bucket}.context7
value:
command: npx
args: ["-y", "@upstash/context7-mcp@latest"]
env:
CONTEXT7_API_KEY: ${CONTEXT7_API_KEY}
Global entries are merged into each project sync. Fetch entries are deduplicated by URL+path; patch entries by key (for replace) or full content (for append). Project entries win on conflict.
Skip the global config with --no-global on the command line or global: false in agpack.yml. Override the default path with AGPACK_GLOBAL_CONFIG.
agpack writes files the user often hand-edits. Three guarantees keep that safe:
.codex/config.toml) round-trip through tomlkit. Comments, key ordering, and whitespace on sections agpack didn't touch survive every sync.agpack sync twice in a row never modifies a file the second time. No mtime churn, no spurious git diffs.replace patch snapshots the value that was at its key before agpack first ran. If you remove that patch from agpack.yml, the next sync restores the snapshot — your hand-written mcpServers.foo survives even if agpack temporarily owned it. Patches agpack created from nothing get deleted; patches that overwrote existing data get reverted. append patches are removed by deep-equality from their target list.The lockfile (.agpack.lock.yml) is the source of truth — commit it alongside agpack.yml so the whole team's syncs converge. Every file write is atomic (write-to-temp-then-rename); agpack never partially writes a file.
targets: [claude, codex, opencode]
dependencies:
mcp:
- key: ${bucket}.context7 # bucket differs per target
value:
command: npx
args: ["-y", "@upstash/context7-mcp@latest"]
If one of those tools needs a genuinely different value shape (not just a different bucket key), add a second entry at the same key scoped to just that tool with target: — see One server, two native shapes.
targets: [claude, opencode]
dependencies:
skills:
- url: https://github.com/owner/claude-specific-skill
target: [claude] # opencode is configured but never gets this one
.env# .env
GITHUB_TOKEN=ghp_xxx
dependencies:
skills:
- url: https://x-access-token:${GITHUB_TOKEN}@github.com/company/private-skills
path: skills/internal
(SSH keys via git@github.com:... are usually simpler — ${GITHUB_TOKEN} in URLs is for CI where SSH isn't available.)
- url: https://github.com/owner/skills-repo
path: skills/my-skill
ref: v1.2.0 # tag, branch, or commit SHA
dependencies:
hooks:
- key: ${bucket}.PreToolUse
strategy: append
value:
matcher: "Write|Edit"
hooks:
- type: command
command: "$${CLAUDE_PROJECT_DIR}/.claude/hooks/lint.sh"
$${CLAUDE_PROJECT_DIR} is written verbatim — Claude Code expands it when the hook fires.
targets: [my-cli]
dependencies:
rules:
- url: https://github.com/owner/rules-repo
path: rules
target_definitions:
my-cli:
rules:
kind: copy-file
path: .mycli/rules
settings:
kind: edit-file
path: .mycli/settings.json
dependencies:
mcp:
- key: ${bucket}.example\.com # writes mcpServers["example.com"]
value: { command: ... }
Delete the entry from agpack.yml, run agpack sync. The lockfile diff drives cleanup: copy-kind files get removed, edit-file replace patches restore the prior value, append patches get the exact item removed.
agpack init [--config PATH] [--global] Scaffold a new config file
agpack sync [--config PATH] [--no-global] [--verbose] Fetch and deploy all dependencies
agpack status [--config PATH] [--no-global] Show installed vs configured state
agpack targets list [--config PATH] [--no-global] Show all available targets and source
agpack targets show <name> [--config PATH] [--no-global] Print the resolved manifest for one
JSON formatting is not preserved. Python's stdlib json has no format-preserving parser. agpack canonicalises (indent=2) on the first write to a JSON file; subsequent syncs are idempotent and won't rewrite it. Hand-edit JSON files in the same shape agpack emits to avoid churn.
Edit-file currently supports only JSON and TOML. Format is inferred from the path extension; other config formats (YAML, INI, custom DSLs) are not patchable.
target_definitions: is full replacement, not extension. Naming a built-in under target_definitions: replaces it wholesale — there is no deep-merge. To add one resource type to Claude (e.g. personas:) you have to restate every resource type Claude already declares. Tip: agpack targets show claude prints the resolved manifest, copy it under target_definitions.claude: as a starting point. The trade-off is intentional — deep-merge across built-in upgrades was judged too magical to ship behind a single config knob.
Cross-target resource types must share a kind. If two targets both declare a commands resource, they have to agree on whether commands is copy-file or copy-directory. (If you only need different values per target, not a different kind — e.g. one MCP server whose config shape differs by tool — reach for target: scoping instead; it's the same resource type, same file, no target_definitions: override needed.) When you genuinely need a different kind, declare separate resource type names and list each entry under the corresponding name. The repetition is the price of clarity:
targets: [tool-a, tool-b]
dependencies:
commands-files: # for targets that want flat files
- url: https://github.com/owner/cmds
path: commands/review.md
commands-patches: # for targets that want config patches
- key: ${bucket}.review
value: { command: "..." }
target_definitions:
tool-a:
commands-files:
kind: copy-file
path: .tool-a/commands
tool-b:
commands-patches:
kind: edit-file
path: .tool-b/config.json
vars: { bucket: commands }
Windsurf and Antigravity MCP configs are global, not per-project. agpack doesn't write user-global config files.
See CHANGELOG.md. On first agpack sync after a major version upgrade, files in the old (pre-upgrade) locations are cleaned up automatically — the lockfile remembers exactly where the previous sync wrote them.
agpack.yml and (optionally) the global config; merges them.${VAR} references from .env files and the shell env (for fetch entries). Patches resolve ${name} per-target at apply time..agpack.lock.yml to diff against the previous state.path is set) and copies files to every target that declares the matching resource type (or the entry's own target: subset, if it has one).target: is only considered for the targets it lists. Files are only written when their text actually changed.GPL-3.0 — see LICENSE.
Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.4.6 | 16 Jul 2026 | 4 |
This describes our collected data; it is not a security assessment.
Observed since 21 Oct, 09:22 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:cfb37545f7331979bfbae939ff3935b36c41cb451168496ef90c2390f82bfd13