Introduction
agentpack pins the skills, plugins, commands, and rules your project feeds to AI coding agents. You declare them once in agentpack.toml, lock them to exact commits in pack.lock, and launch any supported agent with everything already staged in its native format.
Think of it as a package manager for agent configuration: a manifest, a lockfile, a shared verified cache, and reproducible builds — except the artifacts are skills and prompts instead of libraries.
The problem it solves
Agent configuration has become real configuration. A project’s behavior under Claude Code, Cursor, or Codex depends on slash commands, rules files, sub-agent definitions, MCP servers, and skill libraries. Today those artifacts live scattered across GitHub repos and local folders, with no shared versioning story, and each agent expects a different file layout.
So teams copy-paste. The same skill gets pasted into .claude/, then adapted by hand for .cursor/, then forgotten when a third agent shows up. Versions drift between machines. Nothing records which commit of a shared rule set you actually ran. There is no lockfile.
agentpack replaces the copy-paste with a dependency graph.
What it gives you
| Capability | What it means |
|---|---|
| Declarative manifest | agentpack.toml lists direct dependencies with version constraints, modes, and MCP servers |
| Deterministic lockfile | pack.lock pins every package — direct and transitive — to an exact commit and a content hash of its files |
| Shared, verified cache | Each package is fetched once into $AGENTPACK_HOME/cache/<cache_key>/, shared across projects, and checked against the lock before it is staged |
| Per-harness staging | Artifacts are materialized into per-harness staging trees outside your repo — never committed, never symlinked into your real ~/.claude or ~/.cursor |
| Cross-harness conversion | A skill, command, agent, or rule is re-rendered into each harness’s native format, not blindly copied |
| Six launchers | claude, agent (Cursor), opencode, codex, grok, agy (Antigravity) — sync then exec the real binary |
A first taste
# agentpack.toml
name = "my-project"
version = "0.1.0"
[dependencies]
"github.com/anthropics/skills/skills/canvas-design" = { branch = "main" }
"github.com/anthropics/claude-plugins-official/plugins/hookify" = { branch = "main" }
agentpack add anthropics/skills/skills/canvas-design # resolve, lock, sync
agentpack claude # launch Claude Code with both staged
The agent starts with every declared dependency in place — no manual copying, no drift, and nothing leaked into your global agent config.
How the pieces fit
agentpack.toml ──> pack.lock ──> cache ──> staged bundles ──> launch
(you write) (pinned) (fetched) (per-harness) (agent runs)
Read on through Core Concepts to understand each step, or jump to the Quick Start to get something running first.
Source and license
agentpack is MIT-licensed and developed at github.com/olegpustovit/agentpack. It is pre-release: the CLI, lockfile shape, staging layout, and defaults may change without a migration path until a stable release is declared.
Installation
agentpack ships as a single binary. The recommended path is Homebrew; source builds use the Go toolchain.
Installer scripts
On macOS or Linux, download, checksum-verify, and install the latest binary with:
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/olegpustovit/agentpack/releases/latest/download/agentpack-installer.sh | sh
On Windows PowerShell:
irm https://github.com/olegpustovit/agentpack/releases/latest/download/agentpack-installer.ps1 | iex
Both installers write only the executable to $HOME/.local/bin by default and
accept AGENTPACK_INSTALL_DIR to choose another userspace destination. They do
not edit shell profiles or the user PATH. Downloads are verified against the
release’s checksums.txt before installation.
Homebrew (macOS and Linux)
brew install --cask olegpustovit/tap/agentpack
That one line taps olegpustovit/homebrew-tap and installs the cask. The two-step form works too:
brew tap olegpustovit/tap
brew install --cask agentpack
Upgrade later with:
brew upgrade --cask agentpack
Prebuilt binaries
Each release publishes GoReleaser archives for macOS (arm64, x86_64), Linux (x86_64, arm64), and Windows (x86_64), plus a SHA-256 checksums.txt file.
Pick a specific version from the releases page if you don’t want latest.
From source
You need Go 1.26 or newer, then:
git clone https://github.com/olegpustovit/agentpack.git
cd agentpack
go install ./cmd/agentpack
This installs agentpack under GOBIN (or the default Go bin directory); make sure that directory is on your PATH. The repo’s Makefile also offers make install, which builds a stripped binary and copies it to ~/.local/bin (override with make install INSTALL_DIR=/usr/local/bin).
Verify
agentpack --version
Where agentpack keeps its state
All cached content, the metadata index, and per-project bookkeeping live under a single user-wide directory, not inside your repo. When AGENTPACK_HOME is unset, the default is:
- Linux/macOS —
$XDG_DATA_HOME/agentpackifXDG_DATA_HOMEis set, otherwise$HOME/.local/share/agentpack - Windows —
%LOCALAPPDATA%\agentpack
Override it by exporting the variable in your shell profile:
export AGENTPACK_HOME="$HOME/.local/share/agentpack"
A custom AGENTPACK_HOME is useful for a shared cache on a network mount, or for isolating a project’s state. See Environment Variables for the complete list.
Quick Start
Zero to a running agent in a few minutes. The short version: init, add, launch.
1. Initialize a manifest
From your project directory:
agentpack init
This writes a minimal agentpack.toml (identity is two top-level keys, not a [package] table) and an empty v2 pack.lock:
name = "my-project"
version = "0.0.1"
[dependencies]
init fails if agentpack.toml already exists, so it never clobbers your manifest.
2. Add a dependency
add takes a package spec. The most explicit form is a Go-style module ID, but owner/repo/path shorthand and GitHub tree/blob URLs work too:
agentpack add anthropics/skills/skills/canvas-design
agentpack resolves the spec, appends it under [dependencies], refreshes pack.lock, and runs a sync — all in one command:
[dependencies]
"github.com/anthropics/skills/skills/canvas-design" = {}
To pin a constraint, attach a @ref to the spec or edit the entry into table form ({ version = "^1.0.0" }, { branch = "main" }, { tag = "v2.1.0" }). See Your First Manifest. To stage nothing yet, pass --no-sync.
3. Lock and sync explicitly (optional)
add already locked and synced. You only run these by hand for CI, or after editing the manifest directly:
agentpack lock # re-resolve agentpack.toml → pack.lock (network)
agentpack sync # fetch into the cache and rebuild every harness's staging
Commit both agentpack.toml and pack.lock. The first sync downloads everything; later syncs reuse the cache, after checking it against the lock, and are fast.
4. Launch an agent
Each launcher syncs (fast path when nothing changed), stages for that harness, and execs the real binary:
agentpack claude # Claude Code
agentpack agent # Cursor Agent (alias: cursor-agent)
agentpack opencode # OpenCode
agentpack codex # Codex
agentpack grok # Grok
agentpack agy # Antigravity
Anything after the subcommand is forwarded to the underlying binary:
agentpack codex --model gpt-5-codex
Next steps
- Your First Manifest — version constraints, branches, local paths
- Dependency Resolution — how a constraint becomes a pinned commit
- Modes — enable or disable subsets of your packages per task
- Harness guides — what each launcher actually does
Your First Manifest
agentpack.toml lives at your project root and declares two things: the project’s identity and its direct dependencies. It is the source of truth for what to install; pack.lock records the resolved result.
Minimal manifest
Identity is two top-level keys. There is no [package] table.
name = "my-project"
version = "0.0.1"
Module IDs encode the path inside a repo
A dependency key is a module ID — a Go-style path. The in-repo location of the package is part of the ID, not a separate field:
[dependencies]
# Repository root
"github.com/acme/lint-rules" = "^1.2"
# A subdirectory inside the repo (note the trailing path segments)
"github.com/acme/monorepo/agents/python" = "^2.0"
github.com/acme/monorepo/agents/python means the agents/python directory of github.com/acme/monorepo. This is the single most common point of confusion: to pull a subdirectory, put it in the key — do not reach for a path field (that field means something else; see below).
Pinning: short string vs. table
The value can be a short string (a branch, tag, or version constraint) or a table for anything richer:
[dependencies]
# Semver constraint against the repo's tags
"github.com/acme/lint-rules" = "^1.2"
# Track a branch — resolves to its HEAD at lock time (not reproducible over time)
"github.com/acme/experimental" = { branch = "main" }
# Pin an exact tag
"github.com/acme/shared-rules" = { tag = "v2.1.0" }
# Pin an exact commit
"github.com/acme/shared-rules" = { commit = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2" }
A table accepts version, branch, tag, commit, and path. Identity and cache_key always derive from the resolved commit SHA, never from a branch or tag name.
Local path dependencies
The path field points at a directory on your filesystem, relative to the project root. This is for developing a package against a consumer, not for selecting a subdirectory of a GitHub repo:
[dependencies]
"mcp-retrieval" = { path = "../mcp-retrieval" }
On every lock/sync, path dependencies are re-copied from source and re-hashed, so local edits are picked up. Sync will error on a machine where the path is missing and the cache slot is empty.
Version constraint syntax
Constraints are matched against the dependency’s SemVer tags:
| Constraint | Meaning |
|---|---|
"1.2.3" | Exactly 1.2.3 |
"^1.2.3" | >=1.2.3, <2.0.0 (compatible) |
"~1.2.3" | >=1.2.3, <1.3.0 (patch updates) |
">=1.0, <2.0" | Explicit range |
"*" | Any version |
A fuller example
name = "acme-backend"
version = "0.2.1"
[dependencies]
"github.com/anthropics/skills/skills/canvas-design" = { branch = "main" }
"github.com/anthropics/claude-plugins-official/plugins/hookify" = { branch = "main" }
"github.com/acme/monorepo/packages/coding-agent" = "^2.0"
"local-rules" = { path = "../local-rules" }
After editing the manifest by hand, run agentpack lock to refresh pack.lock, then agentpack sync to fetch and stage. (agentpack add does both for you.)
See the Manifest Schema for every field, plus the [modes] and [mcp.servers] sections.
Manifest (agentpack.toml)
agentpack.toml is the human-edited source of truth for a project. It declares direct dependencies, project-local modes, and MCP servers — and nothing else. Transitive dependencies, resolved commits, and cache keys all live in the generated pack.lock.
Project identity
Two top-level keys, written before any table:
name = "my-project" # used in lock metadata and diagnostics
version = "0.0.1" # SemVer string
There is no [package] wrapper. init fills both from the directory name and a default version; you can edit them freely.
[dependencies]
Each key is a module ID; each value is either a short string or a table.
[dependencies]
# Short form: a version constraint, branch, tag, or commit as a bare string
"github.com/acme/lint-rules" = "^1.2"
# Table form: anything richer
"github.com/acme/shared-rules" = { tag = "v2.1.0" }
"local-tools" = { path = "../local-tools" }
Table fields
| Field | Type | Description |
|---|---|---|
version | string | SemVer constraint matched against the repo’s tags |
branch | string | Track a branch; resolves to its HEAD at lock time (not reproducible over time) |
tag | string | Pin an exact tag |
commit | string | Pin an exact commit SHA |
path | string | Local filesystem directory, relative to the project root — for local development, not for selecting a repo subdirectory |
Module ID format
Module IDs are lowercase Go-style paths. The in-repo location of the package is encoded directly in the ID:
github.com/<owner>/<repo> # repository root
github.com/<owner>/<repo>/<p1> # the <p1> subdirectory
github.com/<owner>/<repo>/<p1>/<p2>/... # nested subdirectory
The host is always github.com for remote packages. To depend on a subdirectory of a monorepo, put the subpath in the key:
"github.com/acme/monorepo/packages/agent-rules" = "^1.0"
A human-typed spec may carry an optional @ref suffix (...@v1.2, ...@main), but identity and cache_key always come from the resolved commit, never the ref name.
Remote vs. local
When a dependency has a path field, it is resolved from your filesystem and never fetched from GitHub. The module ID is still used as the package’s identity for deduplication across the dependency graph.
Commit it
Always commit agentpack.toml together with pack.lock. The pair gives every contributor and every CI run identical resolution. Editing the manifest by hand is fine — run agentpack lock afterward to reconcile the lockfile.
Lockfile (pack.lock)
pack.lock records the exact resolved state of your dependency graph. agentpack generates it; you commit it and never edit it by hand. The current format is lockfile version 3; version 2 locks still load. Older [[skills]] / [[plugins]] layouts are rejected.
Why it exists
- Reproducibility — everyone resolves to the same commit, regardless of when they sync or which tags have since been published.
- Completeness — the lock lists every package, both the direct dependencies from your
agentpack.tomland the transitive ones pulled from nestedagentpack.tomlfiles inside fetched packages. - Integrity — each package carries a
content_hashof its file tree. agentpack compares the cache with it before staging and stops on a mismatch. See Integrity and Verification. - MCP servers — the lock records what each staged MCP server runs and, where agentpack can pin it, the exact version. See MCP Servers.
- Auditability — it’s plain TOML, so
git diff pack.lockshows exactly what moved.
Structure
A lockfile has a version marker, a [meta] block, an optional [config] block, a flat list of [[packages]], and a list of [[mcp_servers]]:
lockfile_version = 3
[meta]
name = "my-project"
version = "0.0.1"
[[packages]]
module = "github.com/anthropics/skills/skills/canvas-design"
direct = true
kind = "skill"
url = "https://github.com/anthropics/skills/tree/<40-hex>/skills/canvas-design"
owner = "anthropics"
repo = "skills"
path = "skills/canvas-design"
commit = "<40-hex commit SHA>"
cache_key = "<64-hex cache slot name>"
content_hash = "sha256-tree-v1:<64-hex digest of the file tree>"
[[mcp_servers]]
name = "playwright"
source = "plugin"
launcher = "npm"
status = "pinned"
definition = "sha256:<64-hex digest of the command and arguments>"
requested = "@playwright/mcp@latest"
package = "@playwright/mcp"
version = "0.0.83"
integrity = "sha512-…"
registry = "https://registry.npmjs.org"
Fields per package
| Field | Description |
|---|---|
module | Module ID, matching a key in [dependencies] (or a transitive dependency) |
direct | true for a direct dependency; omitted for a transitive one |
kind | "skill" or "plugin" |
url | GitHub tree URL of the package |
owner, repo, path | GitHub coordinates and in-repo path |
commit | Full 40-hex commit SHA that was resolved |
cache_key | Name of the cache slot under $AGENTPACK_HOME/cache/. It is the SHA-256 of the package identity (github:<owner>/<repo>, path, commit), not of the files, and says nothing about their content |
content_hash | Digest of the package’s file tree, with its algorithm prefix. Absent in a lock written before version 3; such a package is not verified |
name | Plugin name; omitted for skills |
Fields per MCP server
| Field | Description |
|---|---|
name | Server name after the merge |
source | Where the winning definition came from: plugin, manifest, or .agents |
launcher | npm, pypi, docker, remote, or command |
status | pinned, unpinnable, or unpinned |
definition | Digest of the server’s type, command, arguments and URL. A record applies only while it matches the current definition. Environment values are never hashed or stored |
requested | The package or image spec as written, such as @playwright/mcp@latest |
package, version, integrity, registry | What the spec resolved to, when agentpack resolved it |
host | Host of a remote server |
command | Executable of a local command |
allow_unpinned | true when the server was recorded unpinned on purpose with --allow-unpinned-mcp |
reason | Why a server is unpinned or unpinnable |
The optional [config] table holds bookkeeping such as disabled_plugins.
Versions
lockfile_version | Contents | Read by |
|---|---|---|
2 | [[packages]] without content_hash, no [[mcp_servers]] | every build |
3 | version 2 plus content_hash and [[mcp_servers]] | builds that know content hashes |
agentpack writes version 3 as soon as a lock holds a content hash or an MCP record, and version 2 otherwise, so a lock that was never re-locked stays readable by older binaries.
Refreshing the lock
agentpack lock # resolve agentpack.toml → pack.lock, keeping commits already pinned
agentpack lock --update # re-resolve floating pins (branches, floating semver, npm dist-tags)
lock resolves the full graph and rewrites the file. It fetches every package it has no content hash for, records the hash, and records the project’s MCP servers. An MCP server it cannot pin because the registry is unreachable fails the command; --allow-unpinned-mcp records it as unpinned instead. Note that launching a harness does not advance floating pins: launchers run a fast pre-sync that skips re-resolution when inputs are unchanged. Run lock --update (or sync --update-lock) when you actually want a branch or floating constraint to move.
When to commit it
Always — for application projects and shared agent configurations alike. It is what makes “works on my machine” hold across the team and CI. Reusable packages should commit it too, though their downstream consumers resolve independently.
Drift
If pack.lock falls out of sync with agentpack.toml (typically after a hand edit), reconcile with agentpack lock. With an empty [dependencies] table, sync treats the existing lock as authoritative and leaves it alone — useful for hand-authored or test lockfiles.
Dependency Resolution
agentpack lock turns your manifest into a fully pinned graph: every package, direct and transitive, resolved to one concrete commit. This page covers how that happens.
The steps
- Load the manifest. agentpack reads
agentpack.tomland collects[dependencies]. - List available versions. For each remote dependency it needs to resolve a ref, it queries GitHub for the repo’s tags. Tags may be
v1.2.3or1.2.3. - Select a version. For a SemVer constraint, agentpack picks the highest tag that satisfies it. A
tag/commitpin is taken as-is; abranchresolves to that branch’s current HEAD. - Expand transitively. If a resolved package contains its own
agentpack.toml, its[dependencies]are added to the graph and resolved too. This recurses until the graph is complete. - Resolve conflicts. When two packages constrain the same dependency differently, agentpack looks for a version satisfying both. If none exists,
lockfails and names the conflicting packages. - Write
pack.lock. Each selected ref’s commit SHA is recorded along with itscache_key.
lock resolves; it does not download content. Fetching happens in sync.
Version selection
agentpack picks the highest tag satisfying the constraint:
| Constraint | Available tags | Selected |
|---|---|---|
"^1.0" | 1.0.0, 1.2.3, 2.0.0 | 1.2.3 |
"~1.2" | 1.2.0, 1.2.9, 1.3.0 | 1.2.9 |
"1.0.0" | 1.0.0, 1.1.0 | 1.0.0 |
">=1.0, <1.5" | 1.0.0, 1.4.9, 1.5.0 | 1.4.9 |
Branch pins drift
A branch pin resolves to the branch HEAD at lock time. Two people who lock on different days get different commits, so it is not reproducible over time — prefer a version constraint or tag for anything stable:
"github.com/acme/wip" = { branch = "main" }
Because the lockfile stores the resolved commit, a branch pin does not move on its own. It only advances when you explicitly run agentpack lock --update (or sync --update-lock).
Local path dependencies
A path dependency skips version selection entirely. agentpack copies the directory on disk and derives its commit, cache_key and content_hash from the files present at lock time, so local edits are reflected on the next lock/sync.
Package roots and marketplace repositories
Agentpack recognizes a directory as a package root when it contains one of these markers:
SKILL.mdagentpack.toml.claude-plugin/plugin.json,.cursor-plugin/plugin.json, or.codex-plugin/plugin.json.claude-plugin/marketplace.json,.cursor-plugin/marketplace.json, or.agents/plugins/marketplace.json
A marketplace root can represent one logical plugin through separate local source directories for Claude, Cursor, and Codex. Agentpack resolves those paths relative to the marketplace root, verifies that their plugin names agree, and merges them into one canonical cache package before staging. This supports repositories such as:
.claude-plugin/marketplace.json -> ./providers/claude/plugin
.cursor-plugin/marketplace.json -> ./providers/cursor/plugin
.agents/plugins/marketplace.json -> ./providers/codex/plugin
Marketplace source paths must remain inside the repository. A catalog with multiple plugin names is ambiguous for one dependency, so agentpack rejects it; name the plugin with agentpack add <marketplace> --plugin <name> (see the CLI reference), which also follows entries that point at other repositories, or add the plugin’s source directory directly.
Some catalog entries are not plugin directories at all. A Claude Code marketplace may define a plugin in the catalog entry itself ("strict": false, with the entry’s own lspServers, skills and so on) and point source at a directory that holds only a README. The official gopls-lsp, pyright-lsp, lua-lsp and php-lsp entries are of this kind: each is a language server setting for Claude Code and nothing else. Adding such a directory fails with a message that says so and names the catalog and what the entry declares, for example:
cannot install anthropics/claude-plugins-official/plugins/gopls-lsp: it is the marketplace catalog entry
"gopls-lsp", a language server configuration for Claude Code (.claude-plugin/marketplace.json defines
lspServers: gopls runs `gopls`) with no plugin files of its own; …
Nothing is written to the manifest, the lock or the cache. Agentpack stages skills, commands, agents, rules, hooks and MCP servers; it has no language server setting to write for the agents.
Metadata caching and the offline fallback
GitHub ref→commit and tag-list lookups are cached in $AGENTPACK_HOME/cache/db.reddb and reused across add, lock, and sync, which keeps repeat resolutions off the network. When the GitHub REST API fails or is throttled, agentpack falls back to the Git protocol through an embedded Go Git client against https://github.com/<owner>/<repo>.git before resorting to stale cached metadata. There is no external git executable or hard REST API dependency for ref and tag resolution.
To work entirely from a warm cache without touching the network, keep [dependencies] and pack.lock unchanged and run agentpack sync: with an unchanged lock, sync stages from the cache without re-resolving.
Package Cache
agentpack fetches each package once and stores it under a key derived from its repository, path and commit, so the same package at the same commit is never downloaded twice — and is shared across every project on the machine. The key names the slot; the content_hash in pack.lock is what proves the files in it.
Where it lives
Everything sits under the user-wide agentpack home, never inside your repo:
$AGENTPACK_HOME/
cache/
<cache_key>/ # one extracted package tree per repository, path and commit
db.reddb # metadata index + alias map + cached GitHub ref/tag lookups
local/
<owner>/<repo>/... # optional offline mirror (same layout as owner/repo specs)
projects/
<project-hash>/ # per-project bookkeeping (overlay manifests, launch-sync state)
AGENTPACK_HOME defaults to $XDG_DATA_HOME/agentpack (or $HOME/.local/share/agentpack) on Unix and %LOCALAPPDATA%\agentpack on Windows. Override it:
export AGENTPACK_HOME=/data/agentpack
Each cache/<cache_key>/ directory is an extracted tree, named by the cache_key recorded in pack.lock. The key is the SHA-256 of the package identity (github:<owner>/<repo>, in-repo path, commit). It is not a hash of the files and does not change when they do. The same key is reached whenever owner, repo, in-repo path, and commit match, so two projects pinning the same commit share one copy.
agentpack treats a slot as read-only once it is filled. It replaces one only when you ask (sync --repair), or when lock records a first content hash and finds the cached copy differs from a fresh download; both are reported.
How sync uses it
agentpack sync walks pack.lock and, for each package:
- If
cache/<cache_key>/exists, recomputes its tree digest and compares it with the lock’scontent_hash. A mismatch stops the sync; nothing is staged. - If it is missing, downloads the tree from GitHub at the pinned commit (or copies it, for filesystem and
local/mirror specs) into a temporary directory, verifies it the same way, and only then moves it into the slot. - Materializes it into the per-harness staging directories, converting artifacts to each harness’s native format.
See Integrity and Verification for the digest, the error, and the repair path.
Path dependencies are the exception: they are re-copied from source on every lock/sync, so their slot, commit and content hash follow your directory.
Sharing across projects and CI
Because the cache lives under AGENTPACK_HOME rather than in the project, every project on the machine shares it. Two projects depending on the same package at the same commit store it once.
In CI, persist the cache between runs to skip redundant downloads. Key on pack.lock so the cache invalidates when dependencies change:
# GitHub Actions
- uses: actions/cache@v4
with:
path: ~/.local/share/agentpack/cache # or $AGENTPACK_HOME/cache
key: agentpack-${{ hashFiles('pack.lock') }}
Invalidation
The cache is never invalidated automatically: a key names one commit, so a new upstream commit is simply a different key, stored alongside the old one. To replace entries that no longer match the lock, use:
agentpack sync --repair
To force a clean re-fetch of everything, delete the directory and sync again. Every download is verified against pack.lock before it is used:
rm -rf "$AGENTPACK_HOME/cache"
agentpack sync
Integrity and Verification
pack.lock pins two things for every package: the commit it came from and a content hash of the files agentpack cached for it. The commit says which revision you asked for. The content hash lets agentpack prove, every time it reads the cache, that the files are still the ones that were locked.
The content hash
content_hash looks like sha256-tree-v1:<64 hex>. The prefix names the algorithm, so another tool can recompute the value and a future algorithm can sit next to this one.
“No content hash” means exactly one thing: the content_hash key is absent from the entry. A key that is present must be the prefix followed by 64 lowercase hex digits. Anything else (an unknown or missing prefix, a short or uppercase digest, an empty string) makes every command refuse the lock before it fetches or stages anything, and no command rewrites it, lock included:
invalid content_hash in pack.lock: github.com/acme/skills/demo
found "sha256-0000000000000000000000000000000000000000000000000000000000000000"
expected sha256-tree-v1:<64 lowercase hex digits>
lockfile /work/project/pack.lock
fix restore pack.lock from version control, or upgrade agentpack if a newer version wrote it
sha256-tree-v1 covers the package directory as it sits in $AGENTPACK_HOME/cache/<cache_key>/:
-
Collect every regular file and symbolic link below the directory. Directories are not records, so empty directories do not count. Any other file type (device, socket, pipe) is an error.
-
Name each record by its path relative to the directory, with
/separators. -
Sort the records by that path, comparing bytes.
-
Feed one SHA-256 stream with, for each record in order:
kind 0x00 path 0x00 length 0x00 payloadPart Value kindfregular file,xregular file with any execute permission bit,lsymbolic linkpaththe relative path, as bytes lengththe payload length in bytes, as ASCII decimal payloadthe file bytes, or for a link its target text (the link is never followed) -
The digest is the prefix plus the lowercase hex SHA-256.
Other permission bits, ownership, and timestamps are not part of the hash. Moving the directory, copying it to another machine, or restoring it from a CI cache does not change it. Editing a byte, adding, deleting or renaming a file, making a file executable, or swapping a file for a link does.
The same computation in Python:
import hashlib, os, stat
def sha256_tree_v1(root):
records = []
for directory, _, names in os.walk(root):
for name in names:
path = os.path.join(directory, name)
records.append((os.path.relpath(path, root).replace(os.sep, "/").encode(), path))
digest = hashlib.sha256()
for relative, path in sorted(records):
mode = os.lstat(path).st_mode
if stat.S_ISLNK(mode):
kind, payload = b"l", os.readlink(path).encode()
elif stat.S_ISREG(mode):
kind = b"x" if mode & 0o111 else b"f"
payload = open(path, "rb").read()
else:
raise ValueError(f"unsupported file type: {path}")
digest.update(kind + b"\0" + relative + b"\0" + str(len(payload)).encode() + b"\0" + payload)
return "sha256-tree-v1:" + digest.hexdigest()
(os.walk lists a link to a directory under directory names; a complete implementation records those links too.)
What exactly is hashed
The hash is over the tree agentpack materializes, which is the upstream tree at the pinned commit and path after two fixed steps:
- Extraction writes every regular archive entry with mode
0644; execute bits are dropped. A symbolic link is kept as a link (lrecord) when its target is a clean relative path that stays inside the extracted package, which is how a package points one directory at another (.codex/skills/x -> ../../engineering/skills/x). Any other link (absolute, or reaching outside the package) becomes an empty regular file, and no entry is written through a link. A cache entry that agentpack wrote therefore contains onlyfrecords and in-packagelrecords; anxrecord or a link that leaves the package means something changed it afterwards. - Locks written by 0.4.8 and earlier recorded every link as an empty file. Such a
content_hashstill verifies: when the digest above does not match, agentpack also computes it with each in-package link counted as an emptyfrecord and accepts the lock entry if that equals it. An existing lock entry keeps its hash; an entry locked from now on records the links themselves. A cache entry extracted by those versions keeps its empty files until it is fetched again; remove$AGENTPACK_HOME/cache/<cache_key>/and runagentpack syncto get the links. - Layout normalization adds files agentpack derives from the package: plugin manifest stubs for the harnesses the package did not ship one for (
.claude-plugin/,.cursor-plugin/,.codex-plugin/),mcp.jsoncopied from a legacy.mcp.json, and for a single-plugin marketplace repository the merged plugin directory.
To recompute a hash from a Git checkout you have to apply the same two steps. A change to either step changes what is hashed, so it must come with a new algorithm prefix; a test pins the digest of a normalized fixture to enforce that.
For a path dependency the hashed tree is the copy agentpack makes of your directory (regular files only, Git-ignored files left out).
When the hash is recorded
A content hash is only ever taken from bytes fetched for that purpose in the same command. agentpack never reads a hash off a cache entry that was already sitting on disk, because that would bless whatever is there.
| Command | What it does about hashes |
|---|---|
agentpack lock, add, remove, update | Record a hash for every package. A package that is already cached but has no hash in the lock is downloaded again at its pinned commit; if the cached copy differs from the download, the cache entry is replaced and a warning says so. |
agentpack sync and the launchers | Verify. They record a hash only for a package they have to download because it is not cached. |
A hash that is already in the lock is never recomputed or replaced for the same commit. It changes only when the pin itself changes (lock --update, update, or a manifest edit).
An entry must agree with itself
agentpack writes commit, cache_key and content_hash together: the key is derived from the repository, path and commit, and the hash describes that commit’s files. For every GitHub entry that has a content hash, each command recomputes the key and refuses the lock if it does not belong to the entry’s commit. That is what a hand-edited commit looks like, and following it would stage a commit the hash was never taken from:
inconsistent pack.lock entry: github.com/acme/skills/demo
commit 8fc7add0e2b00906426adfb1ab9bf0f0bb40de2d
cache_key 712cf95c…
expected 89acc09c… (the cache_key of that commit)
fix restore pack.lock from version control; move a pin with `agentpack update` or `agentpack lock --update`
If the cache_key is edited to match as well, the entry is consistent but its content hash still describes the old commit, so the fetched or cached files fail verification as described below. Nothing is fetched, staged or rewritten in either case.
What sync may write to pack.lock
sync and the launchers verify the lock; they do not re-resolve it. For an entry that is already locked they never change commit, cache_key, content_hash or url. If nothing the lock records has changed, the file is not rewritten at all.
sync still follows agentpack.toml, and says so on stderr, one line per change:
| Cause | What sync writes | Line printed |
|---|---|---|
| A dependency was added to the manifest | a new entry | pack.lock: added <module> at commit <sha> |
| A dependency was removed from the manifest | the entry is dropped | pack.lock: removed <module> |
| An exact commit pin was changed in the manifest, or a path dependency’s files changed | the entry’s pin | pack.lock: <module> moved from commit <a> to <b> |
| A package without a content hash had to be downloaded | its first content_hash | recorded content hashes for N package(s) … |
| An MCP server was removed or redefined | its record is dropped | pack.lock: dropped the record of MCP server <name> … |
lock, add, remove and update are the commands that re-resolve on purpose (sync --update-lock counts as one). They print the same moved from commit line for every pin they change. Only agentpack lock replaces a lock it cannot parse, with a warning that it did; a lock with an invalid content_hash or an inconsistent entry is not replaced by any command.
Verification
Every command that reads the cache to resolve or stage compares each cache entry with its content_hash first: lock, add, remove, update, sync, sync --verify-only, and any launch that has to sync.
On a mismatch the command stops with a non-zero exit before anything is staged, and prints:
content hash mismatch: github.com/acme/skills/demo
expected sha256-tree-v1:9041ba90…
actual sha256-tree-v1:e2917cc0…
cache /home/you/.local/share/agentpack/cache/e024077a…
commit 1111111111111111111111111111111111111111
repair agentpack sync --repair (re-fetches the commit and verifies it again)
Nothing is changed: the cache entry is left as found so you can inspect it, staging keeps what the last good sync built, and the lock is not touched. agentpack never repairs silently. The same applies when the lock’s digest is well formed but wrong: with a warm cache the entry does not match it, and with an empty cache the download does not match it.
A freshly downloaded tree is checked the same way before it is allowed into the cache. If GitHub (or anything in between) serves different files for the commit the lock pins, or the lock was edited, the download is discarded and the error says the fetched content does not match. --repair cannot fix this; restore pack.lock from version control.
Repair
agentpack sync --repair
For each cache entry that fails verification, --repair downloads the pinned commit again into a temporary directory, verifies it against the lock, and only then replaces the entry. It reports each replacement on stderr with the old and new digests. If the download does not match the lock either, the entry is left alone and the command fails.
Staged files
Each sync records what it staged for every harness: the skills/, commands/, agents/, rules/ and hooks/ trees and the MCP config file of each staged root, with a SHA-256 per file. The record lives under $AGENTPACK_HOME/projects/<hash>/, outside the staging tree.
agentpack sync --verify-only hashes the staged files and compares them with that record. A file that was modified, added or removed fails the command and is named:
staged tree does not match the last sync
modified /tmp/agentpack-29d1…/modes/default/cursor/agentpack-bundle/skills/demo/SKILL.md
added /tmp/agentpack-29d1…/modes/default/cursor/agentpack-bundle/skills/demo/scripts/extra.sh
fix agentpack sync (rebuilds staging from the verified cache and reports what it replaced)
A plain sync always rebuilds staging from the verified cache. When it replaces files that changed since the last sync, it says which:
warning: 2 staged file(s) changed since the last sync and were replaced from the verified cache: modified /tmp/…/SKILL.md; added /tmp/…/extra.sh
Limits of this check:
- The rest of a staged root (credentials, history, harness settings such as
config.toml) is written by the harness while it runs and is not recorded. - Files staged as hard links to your project’s
./.agents/are yours; editing them in place is not reported. - The
syncreport compares file type, size and modification time, because it is about to rebuild anyway. An edit that preserves both is replaced without being named.--verify-onlyhashes content and does see it. --verify-onlyfails if no sync by this version has recorded the staging yet; runsynconce.
Launches that skip the sync
When the manifest, the lock, ./.agents/, the mode and the target are unchanged since the last launch, a launcher skips the sync and stages nothing: the harness starts with the tree the last sync built from a verified cache. On that path agentpack does not hash file contents. It compares a fingerprint of the cache entries’ metadata (each file’s path, type, permission bits, size and modification time) with the one taken after the last full verification. Any visible change ends the fast path and sends the launch through a full sync, which hashes everything and stops on a mismatch.
An edit that keeps a file’s size and restores its modification time is invisible to the fingerprint. It still cannot reach a harness unnoticed, because reaching a harness means re-staging and re-staging always hashes. Set AGENTPACK_FULL_VERIFY=1 to hash on every launch anyway.
A launch that skips the sync does not check the staged files against the record; use sync --verify-only for that.
Locks without content hashes
A lock written before content hashes existed (lockfile_version = 2) still loads and still stages. Its packages are not verified, and every sync and launch says so in one line:
warning: 3 package(s) in pack.lock have no content hash and are not verified; run `agentpack lock` to record them
sync does not rewrite such a lock unless it has to download a package. Run agentpack lock once to record hashes for everything and commit the result; the file becomes lockfile_version = 3, which older agentpack binaries refuse to load (they report content_hash as an unsupported field).
Set AGENTPACK_REQUIRE_VERIFIED=1 (in CI, for example) to make sync and the launchers refuse a lock that still has unverified packages.
What this protects against, and what it does not
It detects:
- A cache entry that was edited, truncated, or partly deleted after it was fetched: by a script, another tool, an agent “fixing” a skill in place, a bad disk, or a poisoned CI cache restore.
- A fetch that returns different files for the pinned commit than the ones that were locked.
- A hand-edited lock entry: a malformed or wrong
content_hash, or acommitthat no longer matches itscache_key. - A staged file that was edited, added or removed:
sync --verify-onlyfails and names it, and the next sync rebuilds staging from the verified cache and reports the replacement.
It does not:
- Vet content. A hash proves the files are the ones that were locked, not that they are safe. Review a skill, plugin or MCP definition before you lock it, and review
git diff pack.lockwhen a pin moves. - Authenticate the publisher. There are no signatures. The first lock trusts what GitHub serves for the commit over TLS; afterwards the hash holds everyone to that.
- Protect the lock. Whoever can rewrite an entry completely (commit, cache key and a matching content hash, or the entry with its
content_hashkey removed) has written a different valid lock. Keep it in version control and review its diffs. - Stop someone with your user account. They can also edit the staged tree, which is not hashed on a launch that skips the sync, or replace the agentpack binary. There is also a short window between verifying a cache entry and copying it into staging.
- Cover path dependencies against their source. They are re-copied from your directory on every
lockandsync, and the hash follows the directory. - Pin everything an MCP server runs. See MCP Servers.
Portable Environments
Agentpack can keep agentpack.toml and pack.lock outside a git checkout so a teammate or CI job can restore the same locked inputs without committing harness-specific setup into every repository.
Three roots
| Object | Role |
|---|---|
| WorkspaceRoot | Agent cwd / checkout. Source of .agents/ inputs and any compatibility-mode overlays. |
| DefinitionRoot | Directory holding agentpack.toml and pack.lock (and definition-relative path deps). |
| Binding | Machine-local map from a workspace path to a definition root, stored under $AGENTPACK_HOME/projects/<hash>/environment.toml. |
Legacy --project-root alone keeps today’s single-root workflow (definition = workspace). Prefer --definition-root and --workspace (or agentpack env use) when they differ.
Commands
# Create an external definition (optionally copied from an existing project)
agentpack env init team --from /path/to/project
agentpack env use team --project /path/to/checkout # alias: env bind
# Explicit selector (fails if missing; never falls through to another env)
agentpack --env team --workspace /path/to/checkout preflight --agent claude
# Share validated manifest, unchanged lock, and optional contract
agentpack env export team.bundle --from team
agentpack env import team.bundle --name team
# Fetch locked cache trees and rebuild staging without amending pack.lock
agentpack env restore
# Pure offline check (no lock/cache/staging/repo writes)
agentpack preflight --agent claude --json --policy local
agentpack --strict-external preflight --agent opencode --policy ci --contract ./contract.json
Exit codes for preflight / probe: 0 ready (including warnings), 2 usage/policy error, 3 violated, 4 required observation unknown, 5 operational failure, 1 unexpected.
Native probe
agentpack sync
agentpack probe --agent claude # plugin list + skill inventory via --plugin-dir
agentpack probe --agent codex # validated positive static-projection discovery
agentpack --strict-external probe --agent claude --policy ci
agentpack --strict-external preflight --agent claude --policy ci --receipt latest
Probes are explicit (never inside pure preflight). Receipts land under $AGENTPACK_HOME/projects/<hash>/probe-receipts/. Claude plugin/skill presence and Codex 0.159.2 positive skill discovery can be observed in isolated controlled_projection scope. Require that scope explicitly in a contract. Claude Code 2.1.296 and Codex 0.159.2/0.159.3 additionally record the workspace_session scope: the catalog of a session started in the checkout, with an observed winner per packaged skill (see contracts). User-level configuration, source digests, model use and complete ambient absence remain unknown. --policy and --strict-external on a probe bind receipt freshness to the later preflight policy, and --receipt latest selects the latest successful current-format record.
agentpack support export RECEIPT_ID --output ./support-capsule.json
Support capsules retain diagnostic codes, evidence grades, coverage, status, and public binary versions. They omit free-form native messages, remedies, arbitrary property values, evidence paths, and private artifact names because these can contain credentials. Review before sharing; nothing uploads automatically.
Supported strict external launch targets (no checkout configuration writes): Claude (--plugin-dir / --settings) and OpenCode (OPENCODE_CONFIG_DIR). Cursor and Antigravity still require workspace overlays in compatibility mode; --strict-external refuses them before writing.
Preflight vs sync --verify-only
sync --verify-only can still resolve dependencies, restore missing cache entries, upsert index rows, drop colliding staged skills, and rewrite the staged manifest. preflight does none of that. Missing cache in offline preflight is a finding with a restore remedy, not permission to fetch.
Inheritance policy
local(default) — report inherited user/global configuration as info/warnings.ci— same findings, with MCP pin gaps and (under--strict-external) undeclared inheritance treated as violations.
A green report means the enumerated configuration checks passed for the selected target and policy. It does not claim the agent is secure, or that LLM answers are reproducible.
Safe sharing
Exports contain agentpack.toml, the unchanged pack.lock, optional contract.json, and a versioned per-file SHA-256 manifest. Bindings, caches, receipts, raw logs, auth, and history are excluded. Export refuses symlinked definition files, inline MCP environment values, recognizable credential arguments or URL credentials, and host-private absolute paths; supply secrets through the launching environment. This conservative check cannot infer every possible secret embedded in an arbitrary string. Review the definition before sharing.
Host-local path dependencies are refused, including relative paths: the current bundle does not include their file trees. Publish these resources as pinned GitHub packages before exporting. A bundle is a portable definition, not an offline cache archive; restore still needs the locked remote inputs or a verified cache.
Import accepts only the documented regular files, rejects duplicates, traversal, symlinks, files over 4 MiB, unsupported schemas, invalid definitions/contracts/locks, and digest mismatches. It validates in a private temporary directory and publishes only when complete. The destination must not already exist, and export also refuses an existing output file. Older bundles without bundle.json must be re-exported. SHA-256 protects against corruption, not an untrusted publisher: inspect a received definition before launching it.
Rebuild recovery and active launches
Service rebuilds hold an exclusive staging lock through verification. An active launch holds a shared lease, so rebuild returns a clear “in use” error rather than modifying that session’s files. The staging-root override is partitioned by definition identity; Grok homes are partitioned by mode. The changed layout requires a fresh sync after upgrading. Existing native credential/history sources continue to be used.
Codex uses published immutable home generations. Other adapters retain stable staging paths: rebuild saves prior trees and the staged-content manifest, restores them on returned errors, and removes backups after successful verification. A durable journal is saved before roots are renamed. If the process is terminated during rebuilding, the next restore/rebuild recovers the prior trees before native state preparation; launch fast paths refuse pending recovery journals. Completed publication is marked before backups are removed, so cleanup interruptions retain the new trees. Recovery accepts only the exact managed roots for that project/mode and refuses symlinked backup directories. This is recoverable publication, not a single crash-atomic pointer switch across every adapter or a guarantee against storage hardware failure. Auth/history symlinks are moved as links and are never followed during rollback. Compatibility-mode workspace overlays remain adapter-managed; strict external mode refuses targets requiring them.
Claude’s allowlist and attribution settings are stored per project/mode and passed as inline --settings JSON. They cannot overwrite another environment’s allowlist, and CLAUDE_CONFIG_DIR stays unchanged so the native credential namespace remains available.
env init --from validates a complete copy before publication and includes an optional contract. Relative local dependencies are rebased to their original absolute source paths so relocation cannot silently select a different local package. These copies remain machine-local and cannot be exported until path dependencies are replaced by pinned remote packages. New destinations publish atomically; existing empty user directories retain their identity, with no-overwrite file publication and rollback on returned failures. Initialization refuses nonempty destinations.
Staging and Bundles
Staging materializes cached packages into the directory layout each harness expects. Every harness has its own conventions, so sync builds a separate staging tree per harness and per mode.
Staging stays outside your repo
Staged trees are deliberately kept out of your project’s Git repository. They are machine-specific, regenerated on demand from the cache, and can be large. agentpack also does not symlink pack content into your real ~/.claude or ~/.cursor — that would leak project-specific pins into your global agent config.
Only agentpack.toml and pack.lock belong in version control. (Two opt-in exceptions exist: the agentpack agent and agentpack agy launchers create a single workspace symlink — see their harness guides.)
Staging root
The default staging root is a per-project directory in the system temp location:
<temp_dir>/agentpack-<project-hash>/
Set AGENTPACK_STAGING_ROOT to pin a stable location — useful when your OS rotates temp directories, or in CI for a fast local disk:
export AGENTPACK_STAGING_ROOT=/tmp/agentpack-staging
Each rebuild creates a private generation under modes/<mode>-g-<id>/. A small modes/<mode>.current file selects the verified generation for new commands. Each harness gets a directory inside it:
<staging-root>/
modes/
default.current # selected generation's directory name
default-g-<id>/
plugins/agentpack-bundle/ # Claude --plugin-dir bundle
opencode/ # OpenCode config root (OPENCODE_CONFIG_DIR)
codex-home/ # legacy Codex staging path
cursor/ cursor-home/ # Cursor plugin tree + fake HOME
grok/ # Grok bundle; home is durable (see Grok guide)
agy/ # Antigravity workspace plugin
A launcher points its harness at the right staged tree by setting an environment variable or CLI flag before running the binary — claude --plugin-dir …, OPENCODE_CONFIG_DIR, CODEX_HOME, GROK_HOME, a fake HOME for Cursor, a workspace --add-dir for Antigravity. Codex uses a separate published home generation per project and mode so running daemon sessions keep their original tree. The harness guides cover each one.
Mutable state is kept outside disposable staging. Codex session directories, prompt history, and SQLite indexes resolve to ~/.codex, while project-scoped MCP OAuth credentials resolve under $AGENTPACK_HOME/projects/<hash>/codex-mcp-oauth/; Grok sessions resolve to ~/.grok/sessions; Cursor state resolves through its real data directory. Claude, OpenCode, and Antigravity already keep session data in their native user locations because agentpack does not redirect those data roots.
What a bundle contains
A bundle is what one package contributes to a staging tree. Packages carry some mix of:
- commands — slash-command definitions
- agents — sub-agent or mode definitions
- skills — reusable instruction sets
- rules — persistent context applied across a session
- MCP servers — merged into each harness’s native MCP config
Cross-harness conversion happens here: a skill authored in one format is re-rendered into every target harness’s native format rather than copied verbatim. Layering order within a tree is user config first, then plugins (by cache_key), then bare skills, then the project’s ./.agents/ overlay — later layers win on the same relative path.
Re-staging
sync rebuilds every harness’s staging from the current cache state:
agentpack sync
The replacement is published only after verification. An active launch keeps its original paths, so another launch, sync, or lock can update the environment without replacing that session’s files. Failed rebuilds leave the published generation selected. Previous staging generations are retained, including MCP support files; they use disk space until removed outside active sessions. The per-generation .staged.json file records the content checked by sync --verify-only.
Commands that take longer than one second report elapsed progress on stderr. lock also names each package as it resolves it. Use --no-progress to suppress those lines, or --quiet to keep only warnings and errors. Progress stops before handing control to a harness or opening the mode editor.
Launchers run a fast pre-sync when agentpack.toml, pack.lock, and ./.agents/ are unchanged since the last successful launch: they compare input and cache metadata and skip the full lock-resolve, re-download, and rebuild. The fast path does not re-hash staged files. Set AGENTPACK_FULL_VERIFY=1 to hash cached package content on every launch; use sync --verify-only to check staged files. Because of that, floating pins do not advance on launch alone — run agentpack sync or agentpack lock --update when you need the lock refreshed.
Modes
A mode is a named, project-local preset that selects which staged content is active. The same pack.lock can be staged several ways — a lean writing profile, a full default, a review profile that drops a noisy MCP server — without touching your dependencies.
Modes are declared under [modes] in agentpack.toml and selected with the global --mode flag:
agentpack --mode writing claude
agentpack --mode review sync
When --mode is omitted, the reserved default mode applies. Each mode stages into its own subtree, so switching modes never rebuilds another mode’s staging.
Anatomy of a mode
[modes.default]
base = "all"
[modes.writing]
base = "none"
enable = [
"package:github.com/mccteam/minecraft-console-client/.skills/humanizer",
"package:github.com/s1mplesonny/technical-writing-skill/technical-writing",
]
disable = ["mcp:linear"]
| Field | Type | Meaning |
|---|---|---|
base | "all" or "none" | The starting point before selectors apply: everything on, or everything off |
enable | string array | Selectors to turn on |
disable | string array | Selectors to turn off |
The model is straightforward: start from base, then apply enable and disable on top. A base = "all" mode with a short disable list is a denylist; a base = "none" mode with an enable list is an allowlist.
Selectors
Selectors name what to toggle:
| Selector | Targets |
|---|---|
package:<module-id> | A whole package |
package-path:<module-id>:<relative-path> | One file inside a package (e.g. a single command) |
mcp:<name> | An MCP server by name |
.agents:<relative-path> | A file from the project’s ./.agents/ overlay |
[modes.lean]
base = "all"
disable = [
"package-path:github.com/acme/heavy-pack:commands/noise.md",
"mcp:filesystem",
]
Managing modes from the CLI
You can edit [modes] by hand, or use the mode subcommands, which preserve TOML formatting:
agentpack mode list # all declared modes (default is always present)
agentpack mode show writing # base + enable + disable for one mode
agentpack mode create review
agentpack mode base review none
agentpack mode enable review package:github.com/acme/shared-rules
agentpack mode disable review mcp:filesystem
agentpack mode delete review # `default` is reserved and cannot be deleted
agentpack mode tui # interactive editor
agentpack mode tui opens a terminal UI for toggling selectors visually. It uses the terminal’s native foreground and background, so it stays monochrome and readable with light, dark, and custom terminal themes.
MCP Servers
Model Context Protocol servers give agents tools and data sources. agentpack collects MCP definitions from your manifest, your packages, and your ./.agents/ overlay, merges them, and writes the result into each harness’s native MCP config during sync — so one definition reaches every agent.
Declaring servers in the manifest
Add servers under [mcp.servers]. Each key is a server name:
[mcp.servers.filesystem]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem"]
[mcp.servers.retrieval]
command = "uvx"
args = ["mcp-retrieval"]
env = { API_KEY = "sk-..." }
| Field | Type | Description |
|---|---|---|
command | string | Executable to launch the server |
args | string array | Arguments passed to the command |
env | string map | Environment variables for the server process |
disabled | bool | Optional; skip this server when set |
Managing servers from the CLI
agentpack mcp add retrieval --command uvx --args mcp-retrieval --env API_KEY=sk-...
agentpack mcp remove retrieval
agentpack mcp list # every server, with provenance (manifest / plugin / .agents) and lock status
add and remove edit [mcp.servers], update the server records in pack.lock, and then sync, unless you pass --no-sync.
The merge pipeline
After packages and the ./.agents/ overlay are staged, sync gathers MCP definitions from three sources and merges them. Later sources win when the same server name appears more than once:
- Plugin definitions — from each staged plugin, ordered by
cache_key, filtered through the active mode - Manifest
[mcp.servers]— your project-level definitions ./.agents/mcp.json— the project overlay
A plugin’s servers are read from every place the vendors’ plugin formats allow, in this order (a later definition of the same server name wins):
| Location | Format it comes from |
|---|---|
mcp.json at the plugin root | Cursor |
.mcp.json at the plugin root | Claude Code, Codex |
mcpServers in .codex-plugin/plugin.json: a path such as ./.codex-plugin/mcp.json, or an inline map | Codex |
mcpServers in .cursor-plugin/plugin.json: a path, an inline map, or an array of them | Cursor |
mcpServers in .claude-plugin/plugin.json: a path, an inline map, or an array of them | Claude Code |
A definition file may wrap its servers in mcpServers or be the bare name → server map; both are published and both are read. A declared path must stay inside the package. A server or a location that cannot be read (a .mcpb bundle, a URL, an entry with neither command nor url) is skipped with a warning that names it; the package’s other servers still load.
A server definition is carried over as type, command, args, env, url, disabled, headers, cwd and its OAuth client settings. The last three are written where the agent’s documented format has a place for them:
| Claude Code | Cursor | Codex | OpenCode | Grok | Antigravity | |
|---|---|---|---|---|---|---|
headers (remote) | headers | headers | http_headers | headers | headers | headers |
cwd (stdio) | no setting | no setting | cwd | cwd | no setting | cwd |
| OAuth client id | oauth.clientId | auth.CLIENT_ID | oauth.client_id | oauth.clientId | no setting | oauth.clientId |
| OAuth client secret | not in the file (MCP_CLIENT_SECRET) | auth.CLIENT_SECRET | no setting | oauth.clientSecret | no setting | oauth.clientSecret |
| OAuth scopes | oauth.scopes | auth.scopes | scopes | oauth.scope | no setting | no setting |
| OAuth callback port | oauth.callbackPort | fixed by Cursor | oauth.callback_port | no setting | no setting | fixed by Antigravity |
The OAuth settings are read in any of the published spellings (oauth with clientId or client_id, Cursor’s auth, a scopes list next to them). Where an agent has no setting, the server is staged for that agent without it and a warning names the server, the agent and the setting. A plugin server’s relative cwd is relative to the plugin and is staged like ${CLAUDE_PLUGIN_ROOT} (see below). In the manifest the same fields are headers, cwd and [mcp.servers.<name>.oauth] with client_id, client_secret, callback_port, scopes, metadata_url.
Header values and OAuth settings may hold credentials. Like env, they are never written to pack.lock, and they are not part of a record’s definition digest; cwd is, when set. Any other field (headersHelper, envFile, …) is left out with a warning that names it.
The merged set is then written in each harness’s native format:
| Harness | Output |
|---|---|
| Claude, Cursor | JSON mcpServers |
| OpenCode | opencode.json |
| Codex, Grok | [mcp_servers] TOML |
| Antigravity | plugin mcp_config.json (remote servers use serverUrl) |
For Cursor’s fake HOME, the merged pack mcp.json is additionally merged with your real ~/.cursor/mcp.json — user-defined entries win on conflict — so agentpack-managed servers coexist with your own.
Servers that run from the plugin’s own directory
A plugin’s server definition may name the plugin’s directory: bun run --cwd ${CLAUDE_PLUGIN_ROOT} start, node ${CLAUDE_PLUGIN_ROOT}/start.mjs. Claude Code expands that variable for a plugin it installed itself; the other agents do not know it, and in agentpack’s merged bundle it would not name the plugin’s files for Claude Code either.
For each package with such a server, sync therefore stages one whole copy of the package under <staging>/modes/<mode>/plugin-roots/<cache_key prefix>/ and writes that path into every harness’s MCP config in place of ${CLAUDE_PLUGIN_ROOT}, ${CURSOR_PLUGIN_ROOT} or ${PLUGIN_ROOT}, in command, args and env. The copy is filtered through the active mode like the rest of the package. The server runs in the copy, not in the verified cache entry, so what it writes next to its files (a node_modules, a database) cannot make the cache fail verification; the copy is rebuilt by each sync.
${CLAUDE_PLUGIN_DATA} and ${PLUGIN_DATA} become $AGENTPACK_HOME/projects/<hash>/plugin-data/<module>/, which survives rebuilds and package updates.
pack.lock records the definition as the package published it, with the variable, so a lock is the same on every machine.
What pack.lock records
MCP definitions live in the manifest, in plugins and in ./.agents/mcp.json, but what a server runs is decided when it starts: npx @playwright/mcp@latest downloads whatever latest means that day. agentpack lock therefore records every merged server in pack.lock under [[mcp_servers]], with an explicit status. agentpack add does the same for the servers the added package brings, and agentpack mcp add for the one it defines:
| Launcher | Recognized from | What is recorded | Status |
|---|---|---|---|
npm | npx, bunx, pnpm dlx, yarn dlx, bun x with a registry package | package, the exact version behind the version or dist-tag you wrote, the registry’s tarball integrity, the registry URL | pinned |
pypi | uvx, uv tool run, pipx run | the requirement as written, and its version if you wrote == | unpinned: version and hash resolution is not implemented |
docker | docker run, podman run | the image as written | pinned if you wrote an @sha256: digest, otherwise unpinned: digest resolution is not implemented |
remote | a url server | the host | unpinnable: the host decides what runs |
command | anything else | the executable | unpinnable: agentpack does not manage what it runs |
pinned means agentpack knows the exact artifact and stages it. unpinned means it could be pinned and is not. unpinnable means nothing agentpack records can fix what runs.
Environment values and arguments are never written to the lock. Each record carries a definition digest of the server’s command, arguments and URL, and applies only while that digest matches; edit a server and its record is stale until the next agentpack lock.
Staging a pinned npm server
For a pinned npm server, sync writes the exact version into every harness’s MCP config instead of the spec you wrote:
agentpack.toml / mcp.json npx @playwright/mcp@latest
staged for every harness npx @playwright/mcp@0.0.83
Your definition is not edited. agentpack mcp list shows both the definition and the pin.
The locked dependency tree of an npx server
A version pin fixes the server’s own package, not the packages it depends on. For a server started with npx, lock therefore also asks npm which packages it would install, without running any package script (npm install --package-lock-only --ignore-scripts in a temporary directory), and records every one of them in pack.lock: its path under node_modules, exact version and registry integrity, with npm’s platform constraints for optional packages. tree_digest is a digest of that list.
[[mcp_servers]]
name = "playwright"
version = "0.0.83"
bin = "cli.js"
tree_digest = "sha256-npm-tree-v1:a856…"
[[mcp_servers.tree]]
path = "node_modules/playwright-core"
version = "1.64.0-alpha-1790635538000"
integrity = "sha512-pNwa…"
Such a server is staged to start through Agentpack instead of npx:
agentpack.toml / mcp.json npx -y @playwright/mcp@latest --headless
staged for every harness agentpack mcp-exec --spec …/mcp-trees/playwright.json -- --headless
The first time the server starts, mcp-exec downloads each tarball of the tree from the registry, checks it against its recorded integrity before writing anything, unpacks it into Agentpack’s cache (cache/npm-trees/<tree digest>) and runs the package’s executable with node and your arguments. No package script runs. Later starts reuse the unpacked tree after checking that its files are unchanged; a tree edited in the cache is unpacked again from the verified tarballs. A tarball whose integrity differs from the lock stops the server and nothing runs. npm is needed only by lock; starting the server needs node and the registry, once per tree.
A teammate who runs the same pack.lock therefore starts the same files. The tree moves only with agentpack lock --update. Locks made before trees were recorded get them on the next agentpack lock, at the version they already pin. Without npm on PATH, or when npm cannot resolve the tree, the server keeps its top-level pin, lock says so, and it starts through npx as before. Servers started by bunx, pnpm dlx or yarn dlx keep their own runtime and are pinned at the top level only. A lock with a tree is written as lockfile_version = 4; earlier releases refuse it with an “unsupported lockfile_version” message rather than start an unlocked tree.
The pin moves only when you ask: agentpack lock --update (or agentpack update with no arguments) resolves dist-tags again. A plain lock, sync or launch keeps the recorded version and needs no registry. If a refresh resolves to the version that is already locked but the registry now reports a different integrity for it, the lock fails and writes nothing.
When the registry is unreachable
agentpack lock and agentpack mcp add need the registry once per new or changed npm server. If it cannot be reached, they fail and write nothing, rather than record a server that looks locked and is not. To proceed anyway:
agentpack lock --allow-unpinned-mcp
agentpack mcp add playwright --command npx --args @playwright/mcp@latest --allow-unpinned
The server is then recorded with status = "unpinned" and allow_unpinned = true, runs as written, and is tried again on the next lock.
agentpack add does not fail on an unreachable registry: the package is added and locked, the server is left without a record, and a warning names it. It then runs as written until agentpack lock can pin it.
sync and the launchers never contact a registry; a locked tree is downloaded by the server’s own start (mcp-exec), once. A server with no current record runs as written, with a warning that names it.
Limits
- Dependencies are locked for
npxservers only. Forbunx,pnpm dlxandyarn dlx, and for annpxserver whose tree npm could not resolve at lock time, only the top-level package is pinned; the runner still resolves its dependencies when it installs it. - Integrity is enforced at launch for a locked tree, by
mcp-exec. For a server pinned at the top level only, the runner downloads the package and checks it against the registry itself; the recorded value letslocknotice a registry that changes a published version. - Native addons and install scripts. No package script runs when a tree is unpacked. A server that needs a compiled addon built by
installorpostinstalldoes not start from a locked tree; record it withnpxarguments that bypass the tree (for example a version range, which staysunpinned). - The registry is the one agentpack can see:
npm_config_registryin the server’senv, then in your environment, thenhttps://registry.npmjs.org. Per-user.npmrcfiles are not read; if yours points elsewhere, set the variable. - Version ranges (
pkg@^1.2), Git and tarball specs, andnpx --packageare recorded asunpinned. Use an exact version or a dist-tag. - PyPI and docker servers are recorded but not resolved. Write
pkg==1.2.3orimage@sha256:…yourself to fix what runs.
Toggling servers per mode
Use the mcp:<name> selector in a mode to enable or disable a server without removing it:
[modes.review]
base = "all"
disable = ["mcp:filesystem"]
See Modes for the full selector syntax.
Claude Code
agentpack claude launches Claude Code with your packages staged as a plugin directory. Claude’s --plugin-dir flag is additive, so this layers on top of your normal configuration rather than replacing it.
What the launcher does
sync builds a single merged plugin bundle, then the launcher runs:
claude --plugin-dir "<staging>/modes/<mode>/plugins/agentpack-bundle" \
--settings '{"includeCoAuthoredBy":false,"attribution":{"commit":"","pr":""},"enabledMcpjsonServers":[]}'
--plugin-dirpoints at the stagedagentpack-bundle.--settingsreceives inline JSON from the project/mode-specific staged settings. It contains attribution controls and the enabled MCP server names. Keeping attribution on removes only the attribution overrides.
Extra arguments are forwarded to claude:
agentpack claude --model opus
agentpack --yolo claude # adds --dangerously-skip-permissions
CLAUDE_CONFIG_DIR is intentionally never set. Claude derives its keychain credential namespace from that variable, so a per-project value would forget your login on every project switch. Your real ~/.claude.json, ~/.claude/settings.json, and keychain entry are reused as-is.
What the bundle contains
agentpack-bundle/
.claude-plugin/plugin.json
commands/ agents/ skills/ # converted markdown artifacts
hooks/ matchers/ core/ ... # raw Claude support directories
.mcp.json # merged MCP servers (see MCP Servers)
agentpack-settings.json # project/mode-specific launch settings
agentpack does not copy your user-scoped ~/.claude/commands, agents, or skills into the bundle. Claude already reads those from $HOME, and copying them would produce duplicate slash commands (e.g. both /code-tutor and /agentpack-bundle:code-tutor).
Artifact handling
| Artifact | In the bundle |
|---|---|
| Commands | commands/<name>.md, Claude command frontmatter |
| Agents | agents/<name>.md, Claude agent frontmatter |
| Skills | skills/<name>/, normalized skill |
| Rules | Best-effort skill fallback (Claude has no first-class rule files) |
| Hooks | Supported — rendered into the bundle’s hooks/ |
| MCP | Merged into .mcp.json at the bundle root |
Plugin-provided guidance is surfaced through a SessionStart hook that emits it as Claude’s additionalContext, so the model sees it at the start of every session. See Cross-Harness Conversion for the full mapping.
Attribution
By default sync writes agentpack-settings.json inside the staged bundle with AI attribution forced off (includeCoAuthoredBy = false, empty commit/PR attribution). The launcher reads this project/mode-specific file and passes its JSON via --settings, which loads at flagSettings scope — above your user, project, and local settings. Inline settings avoid shared mutable allowlists and project-dependent settings filenames in credential handling. No global $AGENTPACK_HOME/claude-settings.json is read or rewritten. Your real config files are never modified. Set AGENTPACK_KEEP_ATTRIBUTION=1 to keep your existing values. See Overrides and Attribution.
Environment
| Variable | Effect |
|---|---|
CLAUDE_CODE_PATH | Path to the claude binary |
AGENTPACK_STAGING_ROOT | Override the staging root |
AGENTPACK_HOME | Override the cache/state root |
See Environment Variables for the complete list.
Cursor Agent
agentpack agent launches the Cursor CLI (cursor-agent) with your packages staged. The subcommand has the alias cursor-agent.
The binary is
cursor-agent, notagent— the bare nameagentcollides with other tools (Grok ships one). Override the path withCURSOR_AGENT_PATH.
What the launcher does
Cursor resolves user-scoped skills, commands, and agents from a HOME-backed .cursor tree. So agentpack runs cursor-agent with a synthetic HOME at <staging>/modes/<mode>/cursor-home, whose .cursor/ symlinks the staged pack assets alongside your real Cursor login and session paths.
It also sets a few environment variables on the child process:
CURSOR_CONFIG_DIR→ the fake home’s.cursor.CURSOR_DATA_DIR→ your real~/.cursorwhen unset, so workspace-trust state survives staging rebuilds.CARGO_HOME,RUSTUP_HOME,DOCKER_CONFIG→ bridged from your real home (unless you already set them) so shell tooling keeps working inside the stagedHOME.
On Linux, agentpack also links both Cursor’s uppercase Electron profile ($XDG_CONFIG_HOME/Cursor) and the CLI’s lowercase authentication profile ($XDG_CONFIG_HOME/cursor) into the synthetic home. This keeps auth.json durable across projects and staging rebuilds.
agentpack does not install pack content into your real ~/.cursor. It may create the durable Cursor profile directories and staged symlinks needed for first-time login/session files to persist; Cursor manages the actual auth, workspace trust, and MCP approval contents.
Workspace and the agents symlink
--workspace defaults to the current working directory where you invoked agentpack — not the pack root. In a monorepo where agentpack.toml lives in a parent directory, that distinction matters; pass --workspace <path> to override.
Cursor reads sub-agents only from resolve(--workspace)/.cursor/agents. So, only agentpack agent creates ./.cursor/agents as a symlink into the staged pack agents/ (when the pack ships agent markdown) and records it in a per-project overlay manifest for safe cleanup. Bare sync and the other launchers leave the workspace alone and remove any stale symlink from a previous agent run. If ./.cursor/agents already exists as a real directory or file, agentpack leaves it and logs a warning. Add .cursor/agents to .gitignore if you don’t want the symlink tracked.
agentpack agent
agentpack agent --workspace ./packages/api
agentpack --yolo agent # adds --force
Cursor’s cursor-agent only accepts --trust in --print/headless mode; agentpack prepends it automatically there.
Staged layout
modes/<mode>/
cursor/ # Cursor plugins layout
.cursor-plugin/marketplace.json
agentpack-bundle/
.cursor-plugin/plugin.json
commands/ agents/ skills/ rules/ hooks/ mcp.json
cursor-home/ # fake HOME
.cursor/ # symlinks to pack dirs + your real cli-config/session files
.config/cursor -> real Cursor Agent CLI profile (Linux auth)
Artifact handling
| Artifact | Staged as |
|---|---|
| Rules | .mdc files, Cursor’s native format (preserved) |
| Commands | Plain markdown |
| Agents / skills | Cursor agent/skill markdown |
| MCP | JSON mcpServers, merged with your real ~/.cursor/mcp.json (your entries win) |
Attribution is forced off via a real cli-config.json file in the staged tree (not a symlink), so agentpack’s writes never bleed back into your real Cursor profile.
Environment
| Variable | Effect |
|---|---|
CURSOR_AGENT_PATH | Path to the cursor-agent binary |
CURSOR_DATA_DIR | Workspace-trust store; agentpack points it at real ~/.cursor when unset |
AGENTPACK_STAGING_ROOT | Override the staging root (use this for a stable path when your OS rotates temp dirs) |
See Environment Variables for the complete list.
OpenCode
agentpack opencode launches OpenCode with a redirected config root. Unlike Claude’s additive plugin dir, OpenCode is configured by pointing it at a different config directory entirely.
What the launcher does
OpenCode searches its config root — opencode.json plus agents/, commands/, modes/, plugins/, skills/ — like the standard .opencode / ~/.config/opencode locations. agentpack stages a complete root and runs:
OPENCODE_CONFIG_DIR="<staging>/modes/<mode>/opencode" opencode
Extra arguments are forwarded:
agentpack opencode --model anthropic/claude-sonnet-4-6
agentpack --yolo opencode # stages opencode.json with "permission": "allow"
What the root contains
opencode/
opencode.json # seeded config + merged MCP + attribution instruction
agents/ commands/ skills/ # converted pack artifacts
plugins/ modes/
The root is seeded from your real ~/.config/opencode/ (opencode.json, agents, commands, modes, plugins, skills) so provider and auth configuration keep working even though the config root is redirected. Converted pack content is then overlaid into OpenCode’s native markdown locations.
Artifact handling
| Artifact | Staged as |
|---|---|
| Commands | OpenCode command markdown |
| Agents | OpenCode agent markdown |
| Skills | OpenCode skill |
| Rules | Best-effort skill fallback |
| MCP | Merged into opencode.json |
Attribution
OpenCode has no first-class attribution setting. agentpack writes an agentpack-no-attribution.md system-prompt file and adds it to instructions[] in the staged opencode.json — prompt-level guidance, the best available lever. Set AGENTPACK_KEEP_ATTRIBUTION=1 to skip it.
Inspecting the staged root
agentpack sync
ls "$AGENTPACK_STAGING_ROOT/modes/default/opencode/" # if AGENTPACK_STAGING_ROOT is set
Environment
| Variable | Effect |
|---|---|
OPENCODE_PATH | Path to the opencode binary |
AGENTPACK_STAGING_ROOT | Override the staging root |
See Environment Variables for the complete list.
Codex
agentpack codex launches the OpenAI Codex CLI with a redirected home. Codex reads its config and skills from CODEX_HOME (default ~/.codex), so agentpack stages a complete home and points the variable at it.
What the launcher does
CODEX_HOME="<generation-home>" codex
Extra arguments are forwarded:
agentpack codex --model gpt-5-codex
agentpack --yolo codex # adds --dangerously-bypass-approvals-and-sandbox
Each project mode gets a complete generation home. Agentpack builds and verifies a replacement before switching new launches to it. Running sessions keep their original home and daemon.
Where the homes are
Codex binds a Unix socket at <home>/app-server-control/app-server-control.sock, and a socket path may be at most about 104 bytes on macOS and 108 on Linux. A home therefore has to sit at a short path, which a staging tree usually is not. The homes are kept in:
$AGENTPACK_STAGING_ROOT/ap-c/<project hash>/<mode>/g-<n>/whenAGENTPACK_STAGING_ROOTis set and that path still leaves room for the socket (a root of up to about 23 bytes on Linux, such as/tmp/apor/srv/agentpack);/var/tmp/ap-c/<project hash>/<mode>/g-<n>/otherwise, including when the configured staging root is too long. This is the one place agentpack writes outside$AGENTPACK_HOMEand the staging root.
Each <project hash> directory holds a .agentpack-project file with the project’s path. A sync in any project removes the directories under /var/tmp/ap-c/ (and the /tmp/agentpack-<hash> staging trees of the default staging location) whose recorded project no longer exists and that no running session holds. Directories written by versions before this marker are not touched. The home is seeded from your real ~/.codex/ (config.toml, skills, themes) so user config keeps working under the redirect.
Credential bridging
Codex stores OAuth/API material in auth.json or in the OS keychain, keyed by the canonical CODEX_HOME path — so a staged path would otherwise miss your keychain entry and force re-login. agentpack avoids copying credentials per project by linking each staged auth.json to a shared source:
- your real
~/.codex/auth.jsonwhen that file already exists, or $AGENTPACK_HOME/shared/codex/auth.json, which agentpack materializes from the real~/.codexkeychain entry (serviceCodex Auth) when credentials live in the keychain.
The staged config.toml is forced to cli_auth_credentials_store = "file", so every project shares refresh-token updates through that one file.
MCP server OAuth is separate from the main Codex login. agentpack stores those tokens per project under $AGENTPACK_HOME/projects/<hash>/codex-mcp-oauth/.credentials.json, links every mode’s staged home to that file, and forces mcp_oauth_credentials_store = "file". The accompanying mcp-oauth-locks/ directory is shared too, so concurrent modes serialize token refreshes correctly. MCP login therefore survives add, sync, staging rebuilds, and temp-directory cleanup without leaking credentials into another project.
After upgrading to this layout, an MCP credential that existed only in the OS keyring or Codex’s encrypted secrets backend requires one final login. Legacy staged .credentials.json files are recovered automatically.
Session history
The generated home is replaceable, but Codex resume state is not. agentpack links staged sessions/, archived_sessions/, and history.jsonl to their native locations under ~/.codex/, and defaults sqlite_home to ~/.codex. Sessions created through agentpack therefore appear in direct codex resume and survive staging cleanup, project/mode changes, and machine restarts.
This is the one thing agentpack creates in your real ~/.codex: every sync (not only agentpack codex) makes sure ~/.codex/sessions/, ~/.codex/archived_sessions/ and ~/.codex/history.jsonl exist, creating them empty when they do not, because the staged home links to them and the links are verified. It happens on a machine where Codex was never run as well. Nothing else under ~/.codex is written; config.toml, skills and themes are only read.
On the first sync after upgrading, agentpack imports surviving history from every old staging mode before rebuilding it. Existing native files always win; differing collisions are retained under $AGENTPACK_HOME/recovery/session-history/codex/ for manual inspection. An explicitly configured sqlite_home is preserved.
Staged layout
codex-home/
auth.json -> ~/.codex/auth.json | $AGENTPACK_HOME/shared/codex/auth.json
.credentials.json -> $AGENTPACK_HOME/projects/<hash>/codex-mcp-oauth/.credentials.json
mcp-oauth-locks/ -> $AGENTPACK_HOME/projects/<hash>/codex-mcp-oauth/mcp-oauth-locks/
sessions/ -> ~/.codex/sessions/
archived_sessions/ -> ~/.codex/archived_sessions/
history.jsonl -> ~/.codex/history.jsonl
config.toml # seeded + stripped legacy attribution + merged [mcp_servers]
skills/
<name>/SKILL.md
Artifact handling
Codex gets the portable skill subset of pack content. agentpack does not synthesize Codex plugin marketplaces from Claude plugins.
| Artifact | Staged as |
|---|---|
| Skills | Codex skill under skills/<name>/ |
| Commands | Skill fallback |
| Agents | Skill fallback |
| Rules | Skill fallback |
| MCP | Merged into [mcp_servers] in config.toml |
Legacy commit_attribution is stripped from the generated config.toml (removed in modern Codex versions). Agentpack no longer injects --no-daemon or disables daemon auto-start. A Codex daemon is tied to one canonical CODEX_HOME and process environment, so each generation has its own daemon. When a generation is replaced, agentpack retains it while a wrapped Codex process is running or its daemon has loaded threads. Once unused, agentpack stops the retired daemon and removes its home during a later sync or launch. Explicit --remote connections remain user-controlled. Set AGENTPACK_KEEP_ATTRIBUTION=1 to preserve any existing commit_attribution.
Codex caches login state inside each daemon. A login or logout made in one generation updates the shared credential file, while an already running daemon in another generation may keep its prior account view until that session ends. New generations read the current credential file.
Environment
| Variable | Effect |
|---|---|
CODEX_PATH | Path to the codex binary |
AGENTPACK_HOME | Cache/state root; also holds shared Codex login state and project-scoped MCP OAuth state |
AGENTPACK_STAGING_ROOT | Override the staging root |
See Environment Variables for the complete list.
Grok
agentpack grok launches the Grok CLI with a redirected home. The installed grok 0.1.219 rejects --plugin-dir and --settings, so agentpack configures it through GROK_HOME and a staged config.toml instead.
What the launcher does
GROK_HOME="$AGENTPACK_HOME/projects/<project-hash>/grok-homes/<mode>/<generation>" grok --cwd <workspace-root>
agentpack injects --cwd when you don’t supply it. Extra arguments are forwarded:
agentpack grok
agentpack --yolo grok # adds --always-approve
The project home is seeded from your real ~/.grok/ (config.toml, skills, agents, commands, plugins) so user config keeps working. Grok credentials are copied into $AGENTPACK_HOME/shared/grok/ after each launch and loaded into every project home before the next launch. Existing ~/.grok credentials and credentials from older agentpack project homes are imported on first use. Grok can replace credential files while refreshing tokens, so a file link alone would not keep them shared.
Grok session transcripts are durable too: staged sessions/ links to ~/.grok/sessions/. This makes agentpack and direct Grok launches share the same resume history across staging rebuilds and machine restarts. On upgrade, surviving session files from old staging modes are imported without overwriting native files; collisions are saved under $AGENTPACK_HOME/recovery/session-history/grok/.
Layout
projects/<project-hash>/grok-homes/<mode>/<generation>/
config.toml # seeded + [plugins].paths + [mcp_servers] + attribution guidance
auth.json # synchronized with shared/grok/auth.json
sessions/ -> ~/.grok/sessions/
<staging>/modes/<generation>/grok/
agentpack-bundle/
plugin.json
commands/ agents/ skills/ rules/
Pack content is rendered into grok/agentpack-bundle/ with a root plugin.json. The staged home links that bundle at plugins/agentpack-bundle and lists agentpack-bundle in [plugins].enabled. Grok trusts plugins in its home plugins/ directory, but it only registers their skills when the plugin is named in [plugins].enabled. A [plugins].paths entry stays disabled and does not register skills. MCP servers are written as Grok-native [mcp_servers] TOML in config.toml.
Hooks are not translated
Observed on 2026-10-12 with Grok 1.0.44: a package’s hooks/hooks.json is copied unchanged into the staged bundle, and a UserPromptSubmit command hook in it fired in non-interactive mode. agentpack does not translate or wrap hooks for Grok, so the notes below, written for Grok 0.2.14, describe what it cannot guarantee.
This is a deliberate limitation, not an omission. Grok’s hooks.json format is Claude-compatible, but grok inspect --json confirms it reads hooks only from the real ~/.grok/hooks — a redirected GROK_HOME is honored for config.toml and MCP but not for hook discovery. Bundle hooks loaded via [plugins].paths come up disabled (they need interactive trust), and grok plugin install --trust writes into the real ~/.grok (pollution). With no isolated, headless path available, Grok’s hook support stays unsupported.
Because Grok also reads Claude-compatible user sources from your real ~/.claude, agentpack’s collision check accounts for both ~/.grok and ~/.claude when removing pack copies that would shadow a user install.
Attribution
grok 0.1.219 has no verified first-class attribution-off setting. agentpack stages prompt-level guidance (AGENTS.md in the staged home) and does not modify your real Grok configuration; Grok writes session state through the durable ~/.grok/sessions/ link.
Environment
| Variable | Effect |
|---|---|
GROK_PATH | Path to the grok binary |
AGENTPACK_STAGING_ROOT | Override the staging root |
See Environment Variables for the complete list.
Antigravity
agentpack agy launches the Antigravity CLI (agy). Antigravity has no config-root override and shares auth and settings with the desktop app under your real profile, so agentpack leaves HOME and ~/.gemini untouched and delivers pack content as a workspace plugin instead.
What the launcher does
agy --add-dir <cwd>
agentpack injects --add-dir with the current working directory (where you invoked agentpack — not the pack root) when you don’t supply it. Extra arguments are forwarded:
agentpack agy
agentpack --yolo agy # adds --dangerously-skip-permissions
The workspace plugin symlink
Pack content is staged into <staging>/modes/<mode>/agy/agentpack-bundle/ with a root plugin.json. Only agentpack agy creates the workspace symlink that exposes it:
./.agents/plugins/agentpack-bundle -> <staging>/.../agy/agentpack-bundle
This follows the same current-working-directory rule as Cursor’s --workspace. The symlink is tracked in a per-project overlay manifest for safe cleanup — bare sync and the other launchers never create it and remove any stale one. Add .agents/plugins/agentpack-bundle to .gitignore if you don’t want it tracked.
Staged layout
agy/agentpack-bundle/
plugin.json
skills/ agents/ commands/ rules/
mcp_config.json # remote MCP servers use serverUrl
MCP servers and agy mcp list
The merged MCP servers are staged as the plugin’s mcp_config.json, the place Antigravity documents for a plugin’s servers (“active when the plugin is enabled”); agy plugin validate .agents/plugins/agentpack-bundle counts them (mcpServers : N processed).
agy mcp list does not show them, and cannot be made to without writing into your profile. Observed on 2026-10-13 with agy 1.3.4, in a temporary HOME:
agy mcp add,remove,enable,disableandlistread and write one file,$HOME/.gemini/config/mcp_config.json, the global configuration “applied to all sessions”.- With that file empty,
agy mcp listprintsNo MCP servers configured.while the workspace holds both.agents/mcp_config.jsonand.agents/plugins/agentpack-bundle/mcp_config.json;agy plugin listprintsNo plugins installed.for the workspace plugin. - No flag of
agyand none ofGEMINI_HOME,GEMINI_CLI_HOME,GEMINI_CONFIG_DIR,AGY_HOME,AGY_CONFIG_DIR,ANTIGRAVITY_APP_DATA_DIR,ANTIGRAVITY_EXECUTABLE_DATA_DIRmoves that file; onlyHOMEdoes, andHOMEalso holds the login and settings Antigravity shares with the desktop app.
So the only ways to make a server appear in agy mcp list are to write it into your real ~/.gemini/config/mcp_config.json, where it would apply to every project, or to run agy under another HOME, where it has no login. agentpack does neither. To use one of the pack’s servers in every Antigravity session, add it yourself with agy mcp add.
Hooks
Hooks are staged as hooks.json at the root of the staged plugin, in Antigravity’s own format: one named hook, agentpack, whose keys are Antigravity’s events. Antigravity loads a plugin’s hooks only from <plugin>/hooks.json (its plugin guide: “native plugins … must place hooks.json directly at the plugin root”), not from hooks/hooks.json, and not in Claude Code’s shape; agy plugin validate reports hooks : 1 processed for the staged bundle (agy 1.3.4, 2026-10-13).
| Hook written for | Staged as | Notes |
|---|---|---|
SessionStart | SessionStart | additionalContext is injected as a system message |
PreToolUse, PermissionRequest | PreToolUse, matcher * | each hook’s own matcher is applied by agentpack; Claude Code tool names match Antigravity’s (Bash = run_command, Read = view_file, Write/Edit = write_to_file/replace_file_content, Grep = grep_search, Glob = find_by_name, WebFetch = read_url_content, WebSearch = search_web) |
PostToolUse | PostToolUse, matcher * | |
UserPromptSubmit | PreInvocation | Antigravity has no prompt-submit event; the hook runs before each model call, with a warning |
Stop | Stop | a blocking result becomes "decision": "continue" |
| any other event | skipped | with a warning that names the event |
Every entry calls agentpack hook-exec dispatch, which runs the package’s handlers in a staged copy of the package and answers in Antigravity’s output contract. A hook that does not object returns no decision: agentpack never answers "allow" for a tool on a hook’s behalf, because for Antigravity that approves the tool without asking you.
Nothing is written to ~/.gemini. The plugin reaches Antigravity through the workspace link ./.agents/plugins/agentpack-bundle, as its skills and MCP servers do, and Antigravity loads workspace hooks once the folder is trusted. What agy plugin validate confirms is that the file is found and parsed; that a session fires the hooks of a workspace plugin can only be seen in a session. An observation with agy 1.0.3 and a Claude-shape hooks/hooks.json found that it did not; that file was in the wrong place and shape, so it does not settle the question for the file staged now.
Attribution
No verified first-class attribution-off setting. agentpack stages an always-apply plugin rule (rules/agentpack-no-attribution.md) as prompt-level guidance and does not modify your real profile.
Environment
| Variable | Effect |
|---|---|
AGY_PATH | Path to the agy binary |
AGENTPACK_STAGING_ROOT | Override the staging root |
See Environment Variables for the complete list.
Cross-Harness Conversion
Every harness names and formats the same ideas differently. agentpack does not copy artifacts verbatim — during sync it parses each markdown artifact from the cached package and re-renders it into every target harness’s native format.
The four artifact types
| Type | What it is |
|---|---|
| command | A named slash command (e.g. /review) |
| agent | A named sub-agent with its own system prompt |
| skill | A reusable instruction set with frontmatter |
| rule | Context applied across a session (Cursor .mdc, Antigravity rules) |
How it works
- agentpack reads the artifact from the cached package, detecting its source format — Cursor plain markdown, OpenCode markdown frontmatter, Claude command/agent/skill frontmatter, or skill frontmatter.
- For each target harness, it re-renders the artifact in that harness’s native shape: the right directory, filename, and frontmatter.
- When a harness lacks a first-class equivalent, agentpack uses a documented fallback rather than dropping the artifact.
Where each artifact lands
| Source | Claude | Cursor | OpenCode | Codex | Grok | Antigravity |
|---|---|---|---|---|---|---|
| command | command frontmatter | plain markdown | command markdown | skill fallback | command | command |
| agent | agent frontmatter | agent markdown | agent markdown | custom agent (agents/<name>.toml) and skill fallback | agent | agent |
| skill | normalized skill | skill | skill | Codex skill | skill | skill |
| rule | skill fallback | .mdc (preserved) | skill fallback | skill fallback | skill fallback | rule (preserved) |
Two harnesses have first-class rules — Cursor and Antigravity — so rules are preserved there. Everywhere else, a rule becomes a best-effort skill, with its original rule scope noted so the intent survives. Codex gets commands as skills. A subagent is staged for Codex twice: as a custom agent file, $CODEX_HOME/agents/<name>.toml with name (the file name), description and developer_instructions (the agent’s body), which is what Codex documents for custom agents, and as a skill of the same name, which Codex versions without custom agents still find.
Where a command, agent or rule becomes a skill, it keeps its name. If the same package already has a skill of that name (or another converted item took it), the converted item is staged as <name>-command, <name>-agent or <name>-rule, then with -2, -3, and a warning says so. Nothing is overwritten.
Where artifacts are found
Artifacts are read from the default directories of every plugin format (skills/<name>/SKILL.md, commands/, agents/, rules/, also under .claude/, .cursor/, .opencode/, .agents/, .grok/), and from the component paths a plugin manifest declares: skills, commands, agents and rules in .claude-plugin/plugin.json, .cursor-plugin/plugin.json or .codex-plugin/plugin.json, each a path or an array of paths. Claude Code adds declared skills to the default directory and replaces it for commands and agents; Cursor replaces; agentpack stages one tree for all agents, so it reads the declared paths in addition to the defaults.
README.md, CHANGELOG.md, LICENSE.md, CONTRIBUTING.md and similar documentation files, written in capitals, are not commands, agents or rules and are not staged.
A declared path, and a symbolic link under a component directory, is followed only while it resolves inside the package ("skills": "./.codex/skills/", where each entry links to ../../engineering/skills/<name>). A path or link that leaves the package, does not exist, or loops is skipped with a warning that names it. Components written inline in a manifest (Claude Code’s commands object) are not staged, also with a warning.
Frontmatter
agentpack writes whatever frontmatter the target format needs (for example, Cursor .mdc metadata), using the artifact’s name and any metadata carried in the source. The instruction body is preserved; only the wrapper changes.
Frontmatter that is not valid YAML as a whole is read one top-level key at a time, as the agents read it: a value that does not parse, such as the unquoted argument-hint: [system] [--source <path>], is kept as the text written. Frontmatter with no readable key leaves the file with no fields and its description is taken from the body.
OpenCode validates every agent file and refuses its whole configuration when one field has another form, so an agent rendered for OpenCode has its Claude Code fields converted:
| Claude Code | OpenCode |
|---|---|
tools: Read, Grep, Bash or a YAML list (an allow list) | tools: object with read, write, edit, bash, grep, glob, list, webfetch, websearch, task, todowrite, skill each true or false |
disallowedTools | the same tools set to false |
color: blue (a name) | the hex value; a value that is neither a known name nor hex is left out |
model: sonnet (an alias) | left out, so the agent uses the session’s model; a provider/model id is kept |
mode other than subagent, primary, all | left out |
A tool with no OpenCode counterpart (an MCP tool name, for example) is left out of the map. Each value left out is named in a warning.
Hooks
A package’s hooks are read from every place the plugin formats define them: hooks/hooks.json, and the hooks field of .claude-plugin/plugin.json, .cursor-plugin/plugin.json and .codex-plugin/plugin.json (a path, an inline object, or an array of them). A file is read in Claude Code’s format ({"hooks": {"Stop": [{"matcher": …, "hooks": […]}]}}, also Codex’s) or in Cursor’s ({"version": 1, "hooks": {"stop": [{"command": …}]}}), whichever its event names belong to.
When a package publishes hooks for more than one agent, each agent gets its own: Claude Code the Claude Code hooks, Cursor the Cursor hooks, Codex the Codex hooks. An agent the package has no hooks for gets the Claude Code hooks, else Cursor’s, else Codex’s, converted event by event.
The event names are the ones in the Claude Code and Cursor hook references (read 2026-10-13). Claude Code has 33:
SessionStart, Setup, UserPromptSubmit, UserPromptExpansion, PreToolUse, PermissionRequest, PermissionDenied, PostToolUse, PostToolUseFailure, PostToolBatch, Notification, MessageDisplay, SubagentStart, SubagentStop, TaskCreated, TaskCompleted, Stop, StopFailure, TeammateIdle, InstructionsLoaded, ConfigChange, CwdChanged, DirectoryAdded, FileChanged, WorktreeCreate, WorktreeRemove, PreCompact, PostCompact, PreModelSwitch, PostModelSwitch, Elicitation, ElicitationResult, SessionEnd.
Cursor has 21: sessionStart, sessionEnd, preToolUse, postToolUse, postToolUseFailure, subagentStart, subagentStop, beforeShellExecution, afterShellExecution, beforeMCPExecution, afterMCPExecution, beforeReadFile, afterFileEdit, beforeSubmitPrompt, preCompact, stop, afterAgentResponse, afterAgentThought, beforeTabFileRead, afterTabFileEdit, workspaceOpen.
| Hook written for | Claude | Cursor | Codex | OpenCode | Antigravity | Grok |
|---|---|---|---|---|---|---|
| a Claude Code event | every event, as written | the ten events Cursor shares (preToolUse, postToolUse, postToolUseFailure, beforeSubmitPrompt, stop, subagentStart, subagentStop, sessionStart, sessionEnd, preCompact); PermissionRequest as preToolUse | the eleven events Codex shares: PreToolUse, PermissionRequest, PostToolUse, PreCompact, PostCompact, UserPromptSubmit, SubagentStart, SubagentStop, Stop, SessionStart, SessionEnd | PreToolUse, PostToolUse, UserPromptSubmit, PermissionRequest, PreCompact | SessionStart, PreToolUse, PostToolUse, Stop; UserPromptSubmit as PreInvocation; PermissionRequest as PreToolUse | not staged (see its page) |
| a Cursor event | the ten shared events | every event, as written, with its matcher, timeout, loop_limit and failClosed | as for the shared Claude Code events | as for the shared Claude Code events | as for the shared Claude Code events | not staged |
How each hook was rendered is reported as an information line (hook for <agent>: …), which --quiet leaves out. A hook whose event the target agent has no equivalent for is skipped for that agent, with a warning that names the event and the hook file; the package’s other hooks and its other content are staged. The same holds for an event name no agent documents, a handler type agentpack cannot run (mcp_tool), and a hook file that does not parse. Command hooks run with CLAUDE_PLUGIN_ROOT, CURSOR_PLUGIN_ROOT and PLUGIN_ROOT set to the staged copy of their package. A command hook in Claude Code’s exec form ("command": "node", "args": ["${CLAUDE_PLUGIN_ROOT}/scripts/hook.mjs"]) keeps its arguments and runs without a shell, with those variables substituted in the program and its arguments. Every staged hook calls agentpack by the absolute path of the executable that staged it, not by whatever agentpack is on the agent’s PATH.
Language servers
A plugin declares language servers in Claude Code’s format: .lsp.json at the plugin root, or lspServers in plugin.json (a path or inline). Anthropic’s language server plugins (pyright-lsp, gopls-lsp and the like) are defined only by their marketplace catalog entry; agentpack takes such an entry as the plugin’s manifest, as Claude Code does, and writes it to the cached package’s .claude-plugin/plugin.json.
| Agent | Where the servers go |
|---|---|
| Claude Code | .lsp.json at the root of the staged bundle |
| Grok | $GROK_HOME/lsp.json (Grok reads a plugin’s .lsp.json only after the plugin is trusted), and [features] lsp_tools = true in its config.toml so that the model gets Grok’s lsp tool, unless the user’s configuration sets it |
| OpenCode | the lsp map of the staged opencode.json: command as [command, args...], extensions from the keys of extensionToLanguage, env, and initialization from initializationOptions; other fields are left out with a warning; agentpack opencode sets OPENCODE_EXPERIMENTAL_LSP_TOOL=true, which gives the model OpenCode’s lsp tool, unless it is already set |
| Codex, Cursor, Antigravity | not written: Codex has no language server setting, Cursor and Antigravity run only their built-in servers; a warning names them |
The language server program itself (for example pyright-langserver) is not installed by agentpack; the agent starts it from PATH.
Skill shadowing
A full plugin at repo path P (same owner / repo / commit) shadows any skill whose path is P or sits under P/. An empty P shadows every skill for that repo at that commit. This keeps a plugin and its bundled skills from staging duplicate copies of the same content.
CLI Commands
agentpack <command> [args]. Global flags may appear before or after the subcommand. Launcher arguments are forwarded unchanged; use -- when an underlying agent flag overlaps an agentpack flag.
Global flags
| Flag | Description |
|---|---|
--project-root <path> | Legacy single root for agentpack.toml/pack.lock (default: search upward from cwd) |
--definition-root <path> | External directory holding agentpack.toml/pack.lock (portable definition) |
--workspace <path> | Checkout / agent cwd for .agents and overlays (default: cwd) |
--strict-external | Refuse Cursor/Agy workspace overlay writes; prefer Claude/OpenCode |
--mode <name> | Select a mode from [modes] (default: the reserved default mode) |
--yolo | Forward each harness’s “skip permission prompts” / full-access flag |
-q, --quiet | Only print warnings and errors |
--no-progress | Disable package progress and elapsed-time updates on stderr |
--debug | Print launcher diagnostics (workspace paths, env overrides, fast-sync skip reason) |
--proxy | Run agentpack claude through the supervised Anthropic-compatible Codex proxy |
-h, --help | Print help |
-V, --version | Print the agentpack version |
When --env, --definition-root, or an env use/env bind binding is set, lock/manifest operations use the definition root while .agents and overlays use the workspace. See Portable Environments.
Help and errors use the terminal’s native foreground and transparent background, with no semantic colors or theme detection. Contextual help is available at every level, for example agentpack mcp add --help.
Shell completion
Generate a completion script using the built-in completion command:
agentpack completion bash
agentpack completion zsh
agentpack completion fish
agentpack completion powershell
The command prints the script to stdout so you can source it directly or save it in your shell’s completion directory.
Lifecycle commands
agentpack init
Create agentpack.toml and an empty pack.lock in the project root, and ensure AGENTPACK_HOME exists. Fails if agentpack.toml already exists.
agentpack init
agentpack init --name my-project --version 0.1.0
agentpack add <spec>
Resolve a package spec, append it under [dependencies], refresh pack.lock, then sync.
agentpack add anthropics/skills/skills/canvas-design
agentpack add github.com/acme/monorepo/packages/rules@v1.2.0
agentpack add ./local-rules
agentpack add anthropics/skills/skills/canvas-design --no-sync
A spec may be a module ID, owner/repo[/path] shorthand, a GitHub tree/blob URL, a single-segment local/alias name, or a filesystem path. An optional @ref pins a branch, tag, or commit. --no-sync skips the sync step. (Requires a manifest.)
--plugin <name> adds one plugin of a marketplace catalog; the spec then names the marketplace repository:
agentpack add anthropics/claude-plugins-official --plugin stripe
agentpack add anthropics/claude-plugins-official@f359d43 --plugin hookify
The catalog entry says where the plugin lives, and agentpack adds that: a directory of the marketplace repository (a relative path or {"source": "local"}), or another GitHub repository ({"source": "github"}, {"source": "url"}, {"source": "git-subdir"}, with its path). An entry’s sha is used as the pin, otherwise its ref; a plugin inside the marketplace repository follows the @ref you gave. The manifest records the plugin’s own repository and path, and pack.lock pins its commit, so later commands do not need the catalog. A name in the catalog’s renames map is followed. An entry that is not a git repository on github.com (npm, another host) is refused with a message that says so.
If add fails at any step, including the sync, agentpack.toml and pack.lock are left exactly as they were.
agentpack remove <spec>
Drop a matching [dependencies] entry, prune mode selectors that targeted it, refresh pack.lock, then sync unless --no-sync.
agentpack remove github.com/acme/monorepo/packages/rules
agentpack lock
Resolve agentpack.toml and rewrite pack.lock (direct + transitive). Network calls for ref/tag resolution; no content download.
agentpack lock # keep commits and MCP versions already pinned
agentpack lock --update # re-resolve floating pins from GitHub and npm dist-tags
agentpack lock --allow-unpinned-mcp # record an MCP server as unpinned if its registry is unreachable
lock records a content_hash for every package (downloading any package it has no hash for) and a record for every MCP server. See Integrity and Verification.
agentpack sync
Ensure the cache and rebuild staging for every harness. Recomputes pack.lock from the manifest when [dependencies] is non-empty.
agentpack sync
agentpack --mode writing sync
agentpack sync --dry-run # report actions without writing
agentpack sync --verify-only # check cache + staging integrity only
agentpack sync --update-lock # re-resolve floating pins while syncing
agentpack sync --repair # re-fetch cache entries that do not match pack.lock
Every sync compares each cache entry with the content_hash in pack.lock and exits non-zero on a mismatch without staging anything. A lock whose entries are malformed or inconsistent is refused the same way, and sync never changes the pins of entries that are already locked; see what sync may write.
--verify-only runs the lock and cache checks without rebuilding, then hashes the staged files and fails, naming each file, if any was modified, added or removed since the last sync. A plain sync rebuilds staging and prints which changed files it replaced. --repair downloads the pinned commit again, verifies it, replaces the cache entry, and reports what it replaced. For a check that must not mutate lock, cache, staging, or the checkout, use preflight instead.
agentpack preflight
Pure offline inspection of a locked environment. Does not download, repair, rewrite pack.lock, rebuild staging, or write workspace overlays. Prints a human summary or --json report with stable finding codes, severity, source, target, and remedy. Optional --contract / contract.json and --receipt ID|PATH|latest for observed coverage / freshness. --mode selects the same mode as restore and launch. Receipt policy and strictness must match the preflight invocation; stale or incomplete receipts fail closed. Strict external launches evaluate the default contract under their staging lease.
agentpack preflight --agent claude
agentpack preflight --agent opencode --json --policy ci
agentpack --env team --workspace . --strict-external preflight --agent claude
agentpack preflight --agent claude --receipt RECEIPT_ID --json
agentpack probe
Explicit native observation (never inside preflight). Claude: plugin list --json + plugin details against the staged bundle. Codex: version, plus validated 0.159.2 and 0.159.3 skill discovery from native prompt input with a disposable home. Claude Code 2.1.296 and both Codex versions also record the workspace_session catalog of a session started in the checkout, including which copy holds each packaged skill’s name. --agent opencode (1.17.11) samples 60 debug skill listings, because OpenCode’s choice between same-named skills varies between runs. Observations carry an explicit scope; they do not prove user-level configuration or model use. Writes a receipt under $AGENTPACK_HOME/projects/<hash>/probe-receipts/.
agentpack sync
agentpack probe --agent claude
agentpack --mode review probe --agent codex --json
agentpack --strict-external probe --agent claude --policy ci
agentpack --strict-external preflight --agent claude --policy ci --receipt latest
agentpack env
Manage portable external definitions under $AGENTPACK_HOME/environments (or an explicit --dir). env bind is an alias for env use. env restore is frozen: exact locked cache + staging rebuild, no pack.lock rewrite.
agentpack env init team --from .
agentpack env use team --project /path/to/checkout
agentpack env status
agentpack env export team.bundle --from team
agentpack env import team.bundle --name team
agentpack --env team --workspace /path/to/checkout --mode review env restore
agentpack env unuse
agentpack env list
agentpack support export
Write a redacted diagnostic capsule from a probe receipt (--output required). Paths, free-form native messages, arbitrary values and secret-bearing digests are omitted; preview notes are printed before sharing.
agentpack support export RECEIPT_ID --output ./support-capsule.json
Launchers
Each launcher runs a fast pre-sync, stages for its harness, and execs the binary. Trailing arguments are forwarded. Launches convert artifacts and hooks only for the selected harness and omit routine hook-rendering messages. Bare agentpack sync builds every harness.
| Command | Launches | Mechanism |
|---|---|---|
agentpack claude | Claude Code | --plugin-dir + --settings |
agentpack agent (alias cursor-agent) | Cursor Agent | synthetic HOME |
agentpack opencode | OpenCode | OPENCODE_CONFIG_DIR |
agentpack codex | Codex | CODEX_HOME |
agentpack grok | Grok | GROK_HOME (+ injected --cwd) |
agentpack agy | Antigravity | injected --add-dir |
agentpack claude --model opus
agentpack --proxy claude
agentpack --yolo codex
agentpack --mode writing claude
See the harness guides for what each one stages.
agentpack eject
IDE extensions and desktop apps (Claude Code in VS Code, the Cursor editor, the Codex extension, the
Antigravity editor) do not start through a launcher. eject syncs, copies the staged tree to
.agentpack/eject/ in the project, points its paths at that copy, and registers it where each agent
reads project configuration:
| Agent | Written |
|---|---|
| Claude Code | .claude/skills, commands, agents; rules in .claude/rules (always-on as they are, file-scoped with paths); servers in .mcp.json, approved by name in .claude/settings.json; hooks in .claude/settings.json. Language servers can only come in a plugin and are not ejected |
| Cursor | .cursor/skills, commands, agents, rules; servers merged into .cursor/mcp.json, hooks into .cursor/hooks.json |
| Codex | skills in .agents/skills, subagents in .codex/agents, servers in .codex/config.toml, hooks in .codex/hooks.json, always-on rules in a marked block of AGENTS.md |
| Antigravity | the bundle copied to .agents/plugins/agentpack-bundle |
Claude Code is given plain project files rather than a plugin: a project-declared marketplace is
installed only after a person accepts it in an interactive session. OpenCode and Grok run in a
terminal; use their launchers. Cursor does not run hooks in non-interactive (-p) runs. Eject refuses, before writing anything, to
replace a file or a setting it did not write. Every file and key it adds is recorded in
.agentpack/eject/manifest.json; --remove takes out exactly those, keeping the rest of each file.
Run it again after changing agentpack.toml.
agentpack eject # every supported agent
agentpack eject claude cursor # only these
agentpack eject --remove
agentpack mcp
Manage [mcp.servers] in the manifest. add/remove update the server records in pack.lock and sync afterward unless --no-sync.
agentpack mcp add retrieval --command uvx --args mcp-retrieval --env API_KEY=sk-...
agentpack mcp add playwright --command npx --args @playwright/mcp@latest --allow-unpinned
agentpack mcp remove retrieval
agentpack mcp list # all servers with provenance and lock status
add resolves an npm server to an exact version and fails if the registry is unreachable; --allow-unpinned records it as unpinned instead. See MCP Servers.
agentpack mode
Manage [modes] in the manifest.
agentpack mode list
agentpack mode show writing
agentpack mode create review
agentpack mode base review none
agentpack mode enable review package:github.com/acme/shared-rules
agentpack mode disable review mcp:filesystem
agentpack mode delete review # `default` is reserved
agentpack mode tui # interactive editor
See Modes for selector syntax.
agentpack extra sync-claude
Reconciles a project’s .claude/skills and .agents/skills directories so a local skill
authored under either one reaches both. Claude Code only discovers project-local skills under
.claude/skills, while the dot-agents convention shares project-local content across every
harness under .agents/skills. This is unrelated to fetched pack content or $STAGING — it only
touches the two real directories in your project.
A skill present on only one side is copied to the other. A skill present on both sides with differing content is reconciled toward whichever copy has the newer file modification time.
agentpack extra sync-claude # reconcile in place
agentpack extra sync-claude --dry-run # show what would change
Environment contracts and evidence
A contract.json beside an external definition expresses checks over its effective plan or an explicitly supplied native probe receipt. Preflight never executes the native CLI. Contract validation rejects unknown categories, properties, targets, selector keys, evidence levels, duplicate IDs, and allowances without a known requirement and rationale.
Property registry
| Category | Properties |
|---|---|
skill, command, agent, rule | presence, source_digest, rendered_digest, source, module, winner, output_path, scope_preserved, source_kind, dropped_fields |
plugin | presence, source_digest, source, module, winner |
runtime | native_version |
planned_write | workspace_configuration |
Names are exact storage names or full artifact IDs; globs are unsupported. Omitted artifacts do not count as present. Package Markdown is parsed and rendered using the same parser, renderer and mode selectors as staging. source_digest hashes source bytes; rendered_digest hashes generated native Markdown bytes. Neither proves that a native CLI discovered those bytes. Logical plugin records describe locked packages, so select minimum_evidence: "declared" for locked plugin identity. Agentpack merges those packages into one native bundle.
Workspace .agents content follows its existing adapter routes. Claude raw overlay bytes and Codex skill/command projections are modeled. Other adapter-specific .agents seeding, and Codex rule guidance aggregation, remain declared rather than reporting an invented generated digest. A generated requirement for such a property stays unknown.
Predicates
present/absent: check a named artifact’s presence, or planned workspace configuration writes. A positive observed check needs a matching observed property. Native absence needs a complete catalog for the requested category and scope, backed by an observedcatalog_completeproperty; a coverage label alone is insufficient.equals: compare the selected property toexpectedusing JSON value equality.source_allowed: comparesource,module, orwinneragainst an expected string or list of strings. It is an identity allow-list, not proof of publisher trust.native_property: equality that always requires observed evidence.coverage_complete: require complete native coverage for the category and scope; incomplete or unobserved coverage stays unknown.no_workspace_write: shorthand for absentplanned_write.workspace_configuration. It checks Agentpack’s plan, not every possible write by a launched program.
minimum_evidence defaults to generated. Observed evidence is never substituted from the package lock or rendered files. severity: "warning" and "info" keep a mismatch nonblocking; "error", "violation", and an omitted severity block. unknown_required defaults to fail; warn and allow relax unknown results explicitly. An allowance applies only to its named violated requirement, records ALLOWED, and cannot manufacture a missing observation.
Native scope
Observation scope defaults to native_catalog. Current safe probes run in a controlled diagnostic projection without the user’s ambient configuration. Their properties carry scope: "controlled_projection". They cannot establish actual-launch ambient exclusion, instruction precedence, source attribution, or sandbox isolation. To check a positive managed skill under that bounded scope, declare it explicitly:
{
"schema_version": 1,
"unknown_required": "fail",
"requirements": [{
"id": "review-loads-in-diagnostic-projection",
"target": "codex",
"selector": {
"category": "skill",
"name": "review",
"property": "presence",
"scope": "controlled_projection"
},
"predicate": "present",
"minimum_evidence": "observed",
"severity": "error"
}],
"allowances": []
}
A controlled positive observation does not require complete ambient catalog coverage. An unscoped observed requirement stays unknown when only controlled evidence is available.
Session scope
scope: "workspace_session" describes a session started in the checkout with the staged configuration and an empty user home. The probe reads the agent’s own catalog and sends no model request: the print-mode init event for Claude Code 2.1.296, debug prompt-input for Codex 0.159.2 and 0.159.3. Unlike the controlled projection it includes skills the checkout itself provides. It still excludes user-level configuration, so absence stays unknown.
For each packaged skill the receipt carries an observed winner:
| Value | Meaning |
|---|---|
package | the staged copy is the only catalog entry with that name |
ambient | Claude Code lists another skill under the bare name; the packaged copy is reachable only as agentpack-bundle:<name> or was dropped from the bundle |
ambiguous | Codex lists several entries with that name |
unstable | OpenCode 1.17.11 listed the staged file in some of 60 sampled runs and another file in others |
{
"id": "review-holds-its-name",
"selector": {"category": "skill", "name": "review", "property": "winner", "scope": "workspace_session"},
"predicate": "source_allowed",
"expected": "package",
"minimum_evidence": "observed"
}
Slash commands and subagents are observed the same way: use "category": "command" or "agent" in the selector. Claude Code lists them in the same init event. For Codex, which has neither, Agentpack renders them as skills and reports the observation under their own category. Hooks are not observed: no agent lists them without running them.
The same requirement without a scope and with minimum_evidence: "generated" checks the planned winner from the workspace scan instead. The two grades disagree in useful ways. The scan flags .claude/skills/<name> for every target although Codex does not read that directory, and the observation shows Codex unaffected. A same-named .agents/skills/<name> replaces the packaged bytes inside the Claude bundle, which the Claude catalog cannot show because it lists names, not bytes; the scan reports it.
Receipt freshness
Current receipts require source and lock identities, mode, workspace input digest, generation digest, policy digest, creation time, adapter revision, executable byte identity, capability revision, termination, and status. Old incomplete receipts fail closed with a re-probe remedy. Failed, cancelled, nonzero, partial/error-status receipts are retained as diagnostics but cannot satisfy contracts.
Preflight compares current identities before attaching properties. Workspace fingerprints cover .agents, workspace .claude / .cursor, .codex/skills and .opencode/skills, root AGENTS.md / CLAUDE.md, user skill roots, and known adapter configuration files; symlink contents are included and cycles fail. Generation identity additionally includes the staged manifest and relevant generated native configuration. A materialization input digest additionally detects an unchanged staged tree built from older inputs. Policy identity includes the selected policy, strict flag and contract bytes. The native executable is hashed without executing it. Upgrading adapter capabilities, changing configuration or instructions, rebuilding staging, or changing the policy invalidates evidence.
Every observed property needs readable retained diagnostic evidence with the recorded byte digest. Missing or modified evidence is unavailable; the digest is a local integrity check, not an attestation. Raw evidence and diagnostic paths are excluded from support exports.
--receipt latest selects the newest successful current-format receipt. It does not fall back silently when that receipt is stale. Where no receipt exists, preflight reports RECEIPT_MISSING and required observations stay unknown. A receipt is local evidence, not a signed attestation. Fingerprints cannot establish unrecorded parent-directory instructions, arbitrary process environment, remote MCP state, model behavior, or external interpreters/dependencies behind an executable. Treat these as outside the closure.
Under CI policy, existing undeclared native configuration and skill roots cause violations. Descriptive capability/inheritance notes are informational; a clean controlled home is not rejected merely because the adapter can inherit configuration.
Comparing receipts
Comparison separates observation categories and scopes, marks differing native versions or failed processes incompatible, and compares JSON values without coercing strings into booleans or numbers. Source, lock, mode, policy, workspace and generation identities appear as separate deltas. Missing properties and properties without observed evidence remain unknown.
Manifest Schema
Complete reference for agentpack.toml.
Top-level shape
name = "my-project" # required
version = "0.0.1" # required
[dependencies] # optional
[modes] # optional
[mcp.servers] # optional
Identity is two top-level keys, not a [package] table.
| Key | Type | Required | Description |
|---|---|---|---|
name | string | yes | Project name; used in diagnostics and lock metadata |
version | string | yes | SemVer string |
[dependencies]
Each entry is a key (module ID) mapped to a value (short string or inline table).
Key format
github.com/<owner>/<repo>
github.com/<owner>/<repo>/<p1>/<p2>/...
Module IDs are lowercase Go-style paths. The host is always github.com for remote packages. A subdirectory inside a repo is part of the key — there is no field for it.
Value: short string
A bare string is a version constraint, branch, tag, or commit:
"github.com/acme/rules" = "^1.2"
Value: inline table
"github.com/acme/rules" = { version = "^1.2" }
| Field | Type | Description |
|---|---|---|
version | string | SemVer constraint matched against the repo’s tags |
branch | string | Track a branch; resolves to its HEAD at lock time (not reproducible over time) |
tag | string | Pin an exact tag |
commit | string | Pin an exact commit SHA |
path | string | Local filesystem directory relative to the project root (local development) |
Local path dependency
A path field makes the dependency local — read from disk, never fetched from GitHub. No version/branch/tag is needed:
"local-rules" = { path = "../local-rules" }
The key is still used as the package’s identity for deduplication.
Version constraint syntax
| Syntax | Semantics |
|---|---|
"1.2.3" | Exactly 1.2.3 |
"^1.2.3" | >=1.2.3, <2.0.0 |
"~1.2.3" | >=1.2.3, <1.3.0 |
">=1.0.0" | At or above 1.0.0 |
">=1.0, <2.0" | Explicit range |
"*" | Any version |
[modes.<name>]
Project-local staging presets. The reserved default mode applies when --mode is omitted. See Modes.
[modes.default]
base = "all"
[modes.writing]
base = "none"
enable = ["package:github.com/s1mplesonny/technical-writing-skill/technical-writing"]
disable = ["mcp:linear"]
| Field | Type | Description |
|---|---|---|
base | "all" or "none" | Baseline before selectors are applied |
enable | string array | Selectors to turn on |
disable | string array | Selectors to turn off |
Selectors: package:<module>, package-path:<module>:<relative-path>, mcp:<name>, .agents:<relative-path>.
[mcp.servers.<name>]
Project-level MCP server definitions, merged into each harness’s native MCP config. See MCP Servers.
[mcp.servers.retrieval]
command = "uvx"
args = ["mcp-retrieval"]
env = { API_KEY = "sk-..." }
| Field | Type | Description |
|---|---|---|
command | string | Executable that launches the server |
args | string array | Command arguments |
env | string map | Environment variables for the server process |
disabled | bool | Optional; skip this server when true |
Complete example
name = "acme-backend"
version = "0.5.0"
[dependencies]
"github.com/anthropics/skills/skills/canvas-design" = { branch = "main" }
"github.com/acme/shared-rules" = { tag = "v2.1.0" }
"github.com/acme/monorepo/packages/agent" = "^2.0"
"local-dev" = { path = "../local-dev" }
[modes.default]
base = "all"
disable = ["package-path:github.com/acme/shared-rules:commands/noisy.md"]
[modes.review]
base = "none"
enable = ["package:github.com/acme/shared-rules", ".agents:rules/backend.mdc"]
[mcp.servers.filesystem]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem"]
Environment Variables
Set these in your shell profile (.bashrc, .zshrc, config.fish) or in CI.
State and staging
AGENTPACK_HOME
The user-wide root for cache, the metadata index, the local/ mirror, and per-project bookkeeping.
Default: $XDG_DATA_HOME/agentpack (or $HOME/.local/share/agentpack) on Unix; %LOCALAPPDATA%\agentpack on Windows.
export AGENTPACK_HOME=/data/agentpack
Point it at a network mount for a shared cache, or somewhere project-specific for isolation.
AGENTPACK_STAGING_ROOT
Root for per-harness, per-mode staging trees.
Default: <temp_dir>/agentpack-<project-hash>.
export AGENTPACK_STAGING_ROOT=/tmp/agentpack-staging
Set this for a stable path when your OS rotates temp directories, or to put staging on a fast local disk in CI.
Behavior toggles
AGENTPACK_KEEP_ATTRIBUTION
Default: unset. Set to 1 / true / yes to keep your existing AI-attribution settings (Co-Authored-By trailers, “Generated with X” footers) in staged harness configs. By default agentpack forces attribution off in staging. See Overrides and Attribution.
AGENTPACK_DOT_AGENTS
Default: enabled. Set to 0 to skip merging the project’s ./.agents/ overlay into harness staging.
AGENTPACK_REQUIRE_VERIFIED
Default: unset. Set to 1 / true / yes to make sync and the launchers refuse a pack.lock that still has packages without a content_hash, instead of staging them with a warning. Useful in CI once the lock has been re-locked. See Integrity and Verification.
AGENTPACK_FULL_VERIFY
Default: unset. Set to 1 / true / yes to hash every cache entry on every launch. By default a launch whose inputs are unchanged stages nothing and only compares cache file metadata with the last full verification. See Integrity and Verification.
Claude proxy diagnostics
AGENTPACK_PROXY_LOG_DIR
Directory for agentpack --proxy claude JSONL diagnostics.
Default: $AGENTPACK_HOME/projects/<project-hash>/proxy-logs.
AGENTPACK_PROXY_LOG_PAYLOADS
Default: unset. Set to 1 / true / yes to include truncated upstream error bodies and payload snippets in proxy logs. Leave unset for sanitized metadata-only logs.
AGENTPACK_PROXY_LOG_MAX_BODY_BYTES
Maximum bytes retained for any payload snippet when payload logging is enabled.
Default: 4096.
AGENTPACK_PROXY_WS_CONNECT_TIMEOUT_SECS
WebSocket TCP connect timeout for proxy upstream connections.
Default: 15.
AGENTPACK_PROXY_WS_IDLE_TIMEOUT_SECS
WebSocket read/write idle timeout for proxy upstream connections.
Default: 300.
Proxy transport, endpoint, and model overrides
| Variable | Default | Purpose |
|---|---|---|
AGENTPACK_PROXY_TRANSPORT | inferred from auth | http, websocket, or auto |
AGENTPACK_PROXY_PORT | 0 | Loopback listen port; 0 selects a free port |
AGENTPACK_PROXY_REQUEST_TIMEOUT_SECS | 300 | Complete upstream request timeout |
AGENTPACK_PROXY_UPSTREAM_URL | Codex auth endpoint | Override the OpenAI Responses endpoint |
AGENTPACK_PROXY_AUTH_JSON | Codex auth discovery | Explicit Codex-compatible auth JSON path |
AGENTPACK_PROXY_BIG_MODEL | gpt-5.5 | Upstream model for Opus-class requests |
AGENTPACK_PROXY_MIDDLE_MODEL | gpt-5.4 | Upstream model for Sonnet-class requests |
AGENTPACK_PROXY_SMALL_MODEL | gpt-5.4-mini | Upstream model for Haiku-class requests |
GitHub access
GITHUB_TOKEN / GH_TOKEN
Default: unset. When set, agentpack sends it as a bearer token for GitHub ref/tag lookups and authenticated downloads. Set one to resolve private repositories or to avoid anonymous API rate limits during heavy resolution. GITHUB_TOKEN takes precedence over GH_TOKEN.
Binary paths
Override the path to each harness’s executable when it isn’t on PATH or you need a specific build:
| Variable | Binary |
|---|---|
CLAUDE_CODE_PATH | claude |
OPENCODE_PATH | opencode |
CODEX_PATH | codex |
CURSOR_AGENT_PATH | cursor-agent |
GROK_PATH | grok |
AGY_PATH | agy |
Summary
| Variable | Default | Purpose |
|---|---|---|
AGENTPACK_HOME | XDG / %LOCALAPPDATA% | Cache and state root |
AGENTPACK_STAGING_ROOT | <temp>/agentpack-<hash> | Staging root |
AGENTPACK_KEEP_ATTRIBUTION | unset | Keep AI attribution in staging |
AGENTPACK_DOT_AGENTS | enabled | Merge ./.agents/ overlay |
AGENTPACK_PROXY_LOG_DIR | project state dir | Claude proxy JSONL diagnostics |
AGENTPACK_PROXY_LOG_PAYLOADS | unset | Include truncated payload snippets in proxy logs |
AGENTPACK_PROXY_LOG_MAX_BODY_BYTES | 4096 | Payload snippet byte cap |
AGENTPACK_PROXY_TRANSPORT | inferred | Proxy upstream transport |
AGENTPACK_PROXY_PORT | 0 | Proxy loopback port |
AGENTPACK_PROXY_REQUEST_TIMEOUT_SECS | 300 | Proxy request timeout |
AGENTPACK_PROXY_UPSTREAM_URL | auth endpoint | Override proxy upstream URL |
AGENTPACK_PROXY_AUTH_JSON | auto | Override proxy auth JSON |
AGENTPACK_PROXY_BIG_MODEL / MIDDLE_MODEL / SMALL_MODEL | built in | Override proxy model mapping |
GITHUB_TOKEN / GH_TOKEN | unset | GitHub auth for private repos and rate limits |
CLAUDE_CODE_PATH … AGY_PATH | — | Override harness binary paths |
Publishing Packages
There is no registry. agentpack resolves packages straight from GitHub, using Git tags as versions. Publishing a package is just pushing a repo and tagging it.
A package is one of two kinds, detected by its layout:
- Skill — a directory containing a
SKILL.md. - Plugin — a directory containing a plugin manifest:
.claude-plugin/plugin.json,.cursor-plugin/plugin.json, or both.
You do not declare your artifacts in a table. agentpack discovers them by reading the package’s content and re-renders them per harness (see Cross-Harness Conversion).
A skill package
The minimal skill is a directory with a SKILL.md:
canvas-design/
SKILL.md
reference.md # any supporting files the skill references
Consumers depend on the directory’s module ID:
[dependencies]
"github.com/acme/skills/canvas-design" = "^1.0"
A plugin package
A plugin carries a manifest plus whatever artifact directories it ships. Layouts are normalized after fetch, so you can expose Claude and/or Cursor manifests:
hookify/
.claude-plugin/plugin.json
commands/ agents/ skills/ rules/ hooks/
mcp.json
Transitive dependencies
If your package depends on other packages, add an agentpack.toml to its root declaring [dependencies]. When someone depends on your package, agentpack reads that manifest at the resolved commit and pulls your dependencies into their graph. The manifest’s [dependencies] table is the only part consulted for transitive resolution.
Versioning and tags
agentpack resolves SemVer tags. Both v1.0.0 and 1.0.0 are recognized:
git tag v1.0.0
git push origin v1.0.0
Suggested bump policy:
| Change | Bump |
|---|---|
| New artifact, backward compatible | MINOR |
| Content-only fix | PATCH |
| Artifact removed/renamed, or format break | MAJOR |
Monorepo packages
Put the package (its SKILL.md or plugin manifest, and an agentpack.toml if it has dependencies) in a subdirectory, and tag the repo normally. Consumers reference the full subpath in the module ID:
[dependencies]
"github.com/acme/monorepo/packages/my-skills" = "^1.0"
agentpack reads from that subdirectory at the tagged commit.
Test before you tag
Develop against a consumer project using a local path dependency, then switch to a version constraint once you’re happy:
[dependencies]
"my-skills" = { path = "../my-skills" }
Run agentpack sync and inspect the staged output (the harness guides show where each tree lives).
Private repositories and rate limits
agentpack reads GITHUB_TOKEN (or GH_TOKEN) when present and sends it as a bearer token for ref/tag lookups and authenticated downloads. Set one to access private repos, and to avoid anonymous API rate limits on heavy resolution:
export GITHUB_TOKEN="ghp_..."
Team Workflows
Patterns for sharing agent configuration across a team.
Commit the manifest and lockfile
Commit both files together:
git add agentpack.toml pack.lock
git commit -m "chore: add agentpack dependencies"
That pair is what makes resolution identical for everyone. Staging trees are machine-local and live outside the repo by default, so there’s nothing to ignore unless you’ve pointed AGENTPACK_STAGING_ROOT inside the project — in which case ignore that path.
Onboarding
A new contributor clones the repo and runs one command:
agentpack sync # fetch everything pinned in pack.lock and stage it
agentpack claude # or any other launcher
No copying config files, no “which version of the rules are we on” — pack.lock answers that.
Updating a dependency
Edit the constraint, re-resolve, commit:
[dependencies]
"github.com/acme/shared-rules" = "^0.4" # was ^0.3
agentpack lock # reconcile pack.lock with the changed manifest
git add agentpack.toml pack.lock
git commit -m "chore: bump shared-rules to ^0.4"
lock reconciles manifest edits. To advance a floating pin (a branch, or a constraint whose range hasn’t changed) to its latest matching commit, use agentpack lock --update. Everyone else pulls the commit and runs agentpack sync.
Shared team packages
Keep team-specific skills and rules in a GitHub repo, tag releases, and depend on it from each project:
[dependencies]
"github.com/acme/team-agent-config" = "^1.0"
For a private repo, every machine and CI runner needs GITHUB_TOKEN (or GH_TOKEN) with read access. Projects stay aligned with agentpack lock && agentpack sync.
A committed project overlay (./.agents/)
To ship project-specific agent content that isn’t a published package, drop it in ./.agents/ at the repo root and commit it. On sync, agentpack merges it into the harness trees that don’t natively read it (the Claude bundle and Codex home), layered last so it wins on conflicts. Shape it like a harness layout: rules/, skills/, agents/, commands/, hooks/, mcp.json, top-level AGENTS.md/CLAUDE.md, and optional claude/ and codex/ subtrees.
Cursor, OpenCode, and Antigravity already discover workspace customization natively, so ./.agents/ is not merged into their staging. Set AGENTPACK_DOT_AGENTS=0 to skip the overlay entirely.
CI
Run agentpack sync before any step that needs staged artifacts, and persist the cache between runs. Key on pack.lock so the cache invalidates when dependencies change:
# .github/workflows/ci.yml
- name: Cache agentpack
uses: actions/cache@v4
with:
path: ~/.local/share/agentpack/cache # or $AGENTPACK_HOME/cache
key: agentpack-${{ hashFiles('pack.lock') }}
- name: Sync agentpack
run: agentpack sync
Merge conflicts in pack.lock
pack.lock is plain TOML and usually merges cleanly, but two branches that both change dependencies can collide. The reliable fix:
- Resolve the conflict in
agentpack.toml(the source of truth). - Run
agentpack lockto regenerate a cleanpack.lockfrom the merged manifest. - Commit the regenerated lockfile.
Overrides and Attribution
How to bend agentpack’s defaults without editing shared packages, plus how attribution is handled in staged configs.
Override a dependency with a local copy
To develop a package and test it in a consumer at the same time, swap the remote dependency for a path:
[dependencies]
"shared-rules" = { path = "../shared-rules" }
sync re-copies the directory on every run, so edits show up immediately. Restore the version constraint and run agentpack lock when you’re done.
Pin an exact commit
When you need a commit that isn’t behind a tag, use the commit field directly — no branch tricks required:
[dependencies]
"github.com/acme/shared-rules" = { commit = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2" }
tag and branch are the other exact-pin options; see Your First Manifest.
Project overlay and per-mode toggles
For project-specific content that isn’t a published package, use the committed ./.agents/ overlay (Team Workflows). To turn whole packages, individual files, or MCP servers on and off per task, use Modes. Both are first-class — reach for them before environment hacks.
Relocate state and staging
export AGENTPACK_STAGING_ROOT="/tmp/agentpack-staging" # stable staging path
export AGENTPACK_HOME="./.agentpack" # project-isolated cache/state
A project-local AGENTPACK_HOME keeps all cached content and bookkeeping inside the repo directory; add .agentpack/ to .gitignore. See Environment Variables for defaults.
Attribution
By default, sync forces AI attribution off in every staged harness config — Co-Authored-By trailers and “Generated with X” footers — so your project doesn’t pick up agent credit lines unintentionally. Your real ~/.claude, ~/.codex, ~/.cursor, and ~/.config/opencode are never modified; only staged configuration copies are touched. Claude settings are project/mode-specific and passed inline at launch.
Set AGENTPACK_KEEP_ATTRIBUTION=1 (or true/yes) to keep your existing values.
How each harness is handled:
| Harness | Mechanism |
|---|---|
| Claude Code | Project/mode-specific staged JSON passed inline via --settings: includeCoAuthoredBy = false, empty commit/PR attribution (loads at flagSettings scope) |
| Codex | Legacy commit_attribution stripped from staged config.toml |
| Cursor | attributeCommitsToAgent = false, attributePRsToAgent = false in the staged cli-config.json (a real file, never your real one) |
| OpenCode | No first-class setting; a system-prompt file is added to instructions[] |
| Grok | No verified setting; prompt-level guidance only |
| Antigravity | No verified setting; an always-apply plugin rule |
Claude and Cursor have real attribution settings, so those are exact. Modern Codex removed commit_attribution, so agentpack strips any legacy setting to avoid unrecognized configuration errors. OpenCode, Grok, and Antigravity expose no such setting, so agentpack falls back to prompt-level guidance — best-effort, not guaranteed.
ICSE evaluation and demonstration
Agentpack’s candidate contribution is an external locked environment with an explainable effective configuration and evidence-graded CI contracts. Locking, external configuration flags, skill installation and environment managers are established techniques; this evaluation must test the added diagnosis value rather than claim those techniques as new.
Reproduce the offline contribution checks
From the repository root:
go build -o ./agentpack ./cmd/agentpack
python3 evaluation/icse/run.py --binary ./agentpack
go test ./internal/harness ./internal/harness/claude ./internal/harness/codex
The Python runner creates disposable external definitions and two relocated checkout directories. It invokes the actual built CLI, not a native agent. Its independent oracles assert:
- equal portable source identities across relocated workspaces;
- an explicit no-workspace-configuration-write contract succeeds for Claude planning;
- observed skill presence without a receipt stays unknown and returns exit 4;
- a strict Cursor workspace overlay requirement fails and returns exit 3;
- lock/definition, checkout, native-home and agentpack state contents, permissions, directories and symlink targets stay unchanged during preflight;
- authored inherited user settings, a mismatched named skill, and an incomplete receipt are rejected while the lock hash stays unchanged.
Static fixture binary-presence checks point at the built Agentpack executable for Claude and Cursor; those paths are never invoked as native agents. Real native discovery uses the separate opt-in tests below. The suite prints its actual outcomes. This is deterministic regression evidence on synthetic fixtures, not a developer study, native discovery experiment or competition benchmark. The temporary workspaces disappear after the run. The initial definition is intentionally empty to keep the offline control independent of network and package registries.
Probe tests use synthetic executable fixtures for invalid outputs, nonzero native exits, empty versions, bounded capture and cancellation. They prove adapter error handling, not native catalog discovery. Native versions outside recorded parser coverage return unknown. Default probes request no model inference, but agentpack does not monitor native network activity or every runtime write; receipts say those effects are unmeasured. Unix cancellation terminates the owned process group. Windows cancellation currently terminates the direct child; descendant-producing Windows probes are not validated.
Native evidence gate
The current Claude parser covers the recorded 2.1.295 and validated 2.1.296 inventory shape. Plugin presence and names discovered in plugin details can be observed; source bytes/digests, ambient user skills and model use remain unknown. A malformed or truncated inventory cannot become an empty successful inventory. Skill inventory coverage remains partial, so it cannot prove complete skill absence.
The Codex 0.159.2 adapter uses debug prompt-input with a disposable HOME/CODEX_HOME, cloud skills disabled, file credential store, and a regular-file copy of staged skills. It does not copy raw user config, credentials, history or hooks. It observes positive names in native developer skill instructions; this is a controlled projection, not proof of the full interactive launch configuration. Symlinked skill inputs are refused. Codex 0.159.3 remains version-only support. Neither adapter can prove model use or complete ambient-skill absence.
Controlled real-native positive/removed-sentinel tests passed on macOS arm64 with Claude 2.1.296 and Codex 0.159.2. Run them explicitly:
AGENTPACK_NATIVE_PROBES=1 go test -v ./internal/harness/claude ./internal/harness/codex -run '^TestNative.*Sentinel$'
AGENTPACK_NATIVE_PROBES=1 go test -v ./internal/cli -run '^TestNativeRestoredPackConformance$'
These tests create benign disposable inputs and isolate native homes. They request no model inference or production MCP/hook execution. They check that the known-positive sentinel appears, that its observation disappears after the authored fixture is removed, and that the fixture workspace stays unchanged. Missing observations remain unknown; this is not a general negative-discovery guarantee. A second integration test freezes an independently authored, content-hashed local skill package, restores it through the actual materialization pipeline for both targets, and confirms the native positive sentinel in both generated configurations. It exercises the full CLI probe, private receipt save and latest-receipt validation. An independently defined empty mode removes the locked skill: neither adapter emits a positive removed-sentinel observation, and a required observed contract stays unknown under partial coverage. It checks equal source identity, unchanged lock bytes and no checkout files. This passed on the same native versions. Native network/global effects are unmeasured. Native writes observed in the spike include Claude config/backups and Codex installation/migration/temp/system-skill files inside diagnostic homes. Sanitized run metadata is in evaluation/icse/native-evidence-macos-arm64.json; bounded stdout/stderr and process status are retained as 0600 JSON evidence under 0700 managed receipt directories, with paths on the local receipt. Support export omits these diagnostic references and raw files.
Before calling the artifact submission-ready, extend the restored-pack controlled sentinel results to held-out ambient/precedence faults and independently grounded source/override expectations, exact executable/version identities, effects and parser fixtures. Use disposable owned diagnostic roots. Do not run production hooks, MCP servers or model requests to obtain deterministic configuration evidence. OpenCode debug config and debug skill are candidates, not implemented validated support: its bootstrap/config loading may install dependencies, write config, run plugins or access remote settings. Full resolved config can contain secrets.
Evaluation protocol
Pin the agentpack revision, native executable versions, platform and every baseline revision. Separate cold/warm runs, static reports, recorded native receipts and live observations. Write expected outcomes independently of the report, and retain clean controls and held-out fault instances.
| Experiment | Ground truth and outcome | Comparison |
|---|---|---|
| External restore in two relocated checkouts | Same locked source identity; expected rendered files; no checkout config writes | Current agentpack; best-practice native external flags with a small wrapper |
| Local artifact or server collision | Authored duplicate sources and independently checked winner; diagnosis/recovery steps | Package-only checks; native diagnostics wrapper |
| Scoped rule or unsupported hook | Declared target support; visible loss/omission; no enforcement claim | Current agentpack renderer checks; Ruler on supported shared tasks |
| Required native observation unavailable | Unknown must fail required observed contract, not masquerade as absence | Native diagnostic wrapper |
| Stale receipt | Change policy, mode, generation, executable or readable ambient inputs independently; receipt rejected | Plain package hashes |
| Skill restoration | Equivalent benign skill inputs and target; restore success and drift detection | Vercel skills on overlapping tasks |
| Concurrent environments | Two projects/modes; active launch plus failed restore; previous generation remains usable | Current agentpack where equivalent |
Record detection precision/recall by fault class, clean false alarms, unknown/coverage rate, restore success, repository writes, runtime overhead and recovery steps. Unsupported baseline tasks are N/A, not failures. Do not infer productivity improvements from command counts alone. A formative, counterbalanced walkthrough with unfamiliar developers is a separate optional study; report sample, recruitment and failures honestly.
Relevant comparison sources: Vercel skills, Ruler, Skilldex preprint, Nix. Credit established mechanisms. No comparative result is supplied by this guide.
A 4:30 demo storyboard
| Time | Action | Evidence on screen |
|---|---|---|
| 0:00–0:35 | Explain a team switching CLI agents while keeping project configuration untouched | Project tree and external definition/lock locations |
| 0:35–1:15 | Select one external environment and inspect generated configuration | Source ID, target, artifact origins, inheritance and planned writes; distinguish generated from observed |
| 1:15–2:00 | Restore in another checkout and run the offline fixture suite | Equal source IDs and full file-tree unchanged checks, not just Git diff |
| 2:00–2:50 | Show a controlled ambient/precedence mismatch | Native observation receipt only if live validated; otherwise explicitly recorded or unknown; package-only baseline misses authored fault |
| 2:50–3:35 | Apply one narrow recovery and check again | Concrete remedy, new generation/receipt; strict contract failure followed by supported pass |
| 3:35–4:10 | Demonstrate unsupported overlay and missing native evidence | Exit 3 versus exit 4 and actionable findings; no silent checkout writes |
| 4:10–4:30 | Show public artifact, tested-version matrix and limits | Config reproducibility is not deterministic LLM behavior; two-adapter native gate status |
Only film commands that exist in the released artifact. The deterministic runner is runnable now; native fault/recovery scenes remain contingent on real validation. Do not replace an unavailable live observation with an unlabeled mock.
The ICSE 2027 Tool Demonstration call requires a usable public tool and a 3–5 minute video. Acceptance is not guaranteed by a completed checklist. A narrow truthful static contribution is preferable to unsupported native-observation claims.