Provisioning
September 6, 2026 · View on GitHub
aide can declare which plugins and MCP servers a context expects, then
reconcile that declaration against what is actually installed in the
agent. The model is the same Terraform-style "describe the desired
end state, then plan and apply": you write a config, run aide sync --plan to see the diff, and aide sync to make it real.
Why this exists
Plugin/MCP state is project-shaped, but agents store it machine-shaped: a new laptop means reinstalling everything by hand and hoping you didn't miss one, a new teammate gets a setup README instead of a source of truth, and a client project sharing an agent binary with a personal project means uninstalling and reinstalling plugins as you switch - or polluting every context with every plugin you've ever needed.
Declarative provisioning fixes this by treating plugins/MCP the way Terraform treats infrastructure: declared in version-controlled config, reconciled with a planned diff, recorded as state on disk. One command brings any machine - yours, a teammate's, a fresh container - to the exact state your config says it should be in.
Why per-context profiles matter
Every coding agent reads from one fixed config dir by default
(~/.claude, ~/.gemini, ~/.copilot, etc.), holding plugins, MCP
servers, session history, and credentials for every project you run it
against. That means session history and MCP servers bleed across
projects, plugins with conflicting assumptions can't coexist per-project,
and every project shares the same model-provider credentials even when a
client requires separate billing.
Every major agent has the same workaround: an env var that swaps the
entire config tree - CLAUDE_CONFIG_DIR for Claude, GEMINI_HOME for
Gemini, CODEX_HOME for Codex, COPILOT_HOME for Copilot. Doing this by
hand means remembering each agent's env-var name, tilde-expanding paths
some agents don't expand themselves, and wiring the env var into your
shell init, your launch script, and the sandbox rules that grant access
to the custom dir - three places, three chances to drift.
aide's profile: <name> field collapses all of that. You write:
contexts:
work:
agent: claude
profile: work
and the driver does the rest: derives ~/.claude-work, sets
CLAUDE_CONFIG_DIR at launch, propagates the env through
aide sync / aide adopt / aide plugin list so they all
target the right directory, and emits the seatbelt rule
that grants read/write access to the profile dir. Switch context
and a different profile takes over.
This is what makes the provisioning workflow above actually safe:
each context's aide sync writes into the right config dir
without your having to think about which env var that agent
happens to use this week.
What gets managed
Two top-level config blocks:
plugins:- agent plugins or marketplaces (one shape per entry)mcp_servers:- MCP server definitions
Both live alongside agents: and contexts: in your
~/.config/aide/config.yaml. Contexts opt in to specific entries via
overrides.
The reconciler walks four state sources:
- Declared - what your config says should exist
- Installed - what the agent reports installed (via the driver's CLI surface)
- Managed - what aide previously installed (recorded in
~/.local/state/aide/managed.json) - Adopted - items installed by hand that you want aide to manage going forward
The diff between these produces the plan.
Polymorphic plugin schema
The plugins: block reads each entry's value shape to decide what the
key means - no type: discriminator. Three shapes:
plugins:
# list value → marketplace + plugins to install from it
steveyegge/beads: [beads]
jskswamy/claude-plugins: [craft, devenv, jot, refactor]
# string value → URL-direct install ref (Gemini-style agents)
my-org/tool: "github:my-org/internal-tool"
# null value → declare-only marketplace (ensure cached, install
# nothing from it; useful when you want the marketplace registered
# before adding plugins one at a time)
obra/superpowers-marketplace: ~
For marketplace shape, the key is a repo path (owner/repo or a
full URL). For URL-direct shape, the key is a plugin name the user
picks for readability.
MCP servers schema
mcp_servers: always uses inline-table form (one shape):
mcp_servers:
postgres:
command: postgres-mcp
args: ["--port", "5432"]
rfctl:
command: rfctl
args: [serve]
github:
url: "https://api.githubcopilot.com/mcp"
Each entry has either command+args (stdio MCP) or url (HTTP MCP).
env: per-server is also supported.
Per-context overrides
By default every context sees the full top-level set. To customise per-context, use three delta keywords with deterministic composition:
contexts:
default:
agent: claude
# inherits everything declared at the top level
work:
agent: claude
profile: work
plugins:
exclude:
- obra/superpowers-marketplace/double-shot-latte
- jskswamy/claude-plugins/refactor
extra:
my-org/internal: [client-tools]
oss:
agent: claude
profile: oss
plugins:
only:
- jskswamy/claude-plugins: [commit-tools, refactor]
- steveyegge/beads: [beads]
Three keywords:
exclude:- subtract from the inherited set. Path syntaxrepo/pluginreaches inside a marketplace entry, removing one plugin without touching the rest.extra:- add on top of inherited entries.only:- replace the inherited set entirely with this list.
The same three keywords apply to mcp_servers: blocks per-context.
Entries use the full inline-table form (just like the top-level schema),
and env: values support {{ .secrets.<name> }} templating resolved
from the context's secret: store at sync time.
aide sync only actually decrypts the secrets store and re-resolves
these templates when something relevant changed since the last
successful sync: either config.yaml or the encrypted secrets file
itself. When neither changed, sync skips decryption entirely and
substitutes the values already installed in the agent's config instead,
so routine no-op sync runs don't require the age key to be available.
One consequence: if an installed secret value is manually edited or
corrupted directly in the agent's own config, sync no longer heals it
automatically on the next run the way it used to. Run aide sync --force-secrets to force re-resolution from the secrets store and
repair drift like that on demand.
contexts:
work:
agent: claude
profile: work
secret: work
mcp_servers:
# add GitHub + Jira on top of the inherited set
extra:
github:
command: npx
args: ["-y", "@modelcontextprotocol/server-github"]
env:
GITHUB_PERSONAL_ACCESS_TOKEN: "{{ .secrets.github_token }}"
jira:
command: npx
args: ["-y", "@modelcontextprotocol/server-jira"]
env:
JIRA_URL: "https://my-org.atlassian.net"
JIRA_API_TOKEN: "{{ .secrets.jira_token }}"
oss:
agent: claude
profile: oss
mcp_servers:
# OSS work only needs GitHub - replace the inherited set
only: [github]
Keep the literal token inline (GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_…")
only for throwaway tests; prefer the {{ .secrets.<name> }} form so the
value stays out of version-controlled config.
CLI commands
Four commands drive the workflow.
aide sync
Plan-then-apply reconciliation.
aide sync # interactive: shows plan, asks for confirmation
aide sync --plan # plan-only; never mutates state
aide sync --yes # non-interactive (CI / scripts)
aide sync --context oss # operate on a specific context (default: matched-by-CWD)
Marketplace adds are sequenced before plugin installs. Each successful
operation records an inverse in an in-memory journal; on any failure
the engine walks the journal in reverse and rolls back, then prints
the failing op and a retry hint. State is persisted to
managed.json atomically and only on full success.
Sample plan output:
Plan for context work (agent: claude):
+ install marketplace jskswamy/claude-plugins
+ install plugin commit-tools (from jskswamy/claude-plugins)
+ install plugin craft (from jskswamy/claude-plugins)
- uninstall plugin refactor (no longer declared)
unmanaged plugin gopls-lsp (installed in agent, not declared,
not previously managed - run `aide
adopt` to bring under management)
aide adopt
Promote agent-installed but undeclared items into config.yaml.
aide adopt # interactive: prompts per item
aide adopt --yes # accept everything
aide adopt --context work
For marketplace-class agents, adopted plugins land under the right
repo key in list-valued form (looked up via the driver's
InstalledMarketplaces). For URL-direct agents (Gemini), adopted
plugins get string-valued entries.
aide adopt is the bridge between manual setup and managed state -
if you previously installed plugins by hand, run adopt once and
subsequent aide sync runs treat them as known.
aide plugin list
Three-column view (declared / installed / managed) per context.
aide plugin list # current context
aide plugin list --context work
For marketplace agents the output includes a MARKETPLACES section
first, surfacing the agent's canonical marketplace name (e.g.
beads-marketplace for steveyegge/beads) and flagging installed-
but-undeclared marketplaces as unmanaged.
aide mcp list
Same shape as aide plugin list but for MCP servers.
aide mcp list
aide mcp list --context work
State file
aide records what it manages in ~/.local/state/aide/managed.json.
Schema (abbreviated):
{
"version": 1,
"contexts": {
"default": {
"config_hash": "sha256:...",
"synced_at": "2026-05-18T...",
"plugins": {...},
"mcp_servers": {...},
"marketplaces": {...}
},
"work": { ... },
"oss": { ... }
}
}
config_hash and synced_at are per-context - a successful
sync of one context never silences drift signals for another. The
file is written atomically (rename-into-place) and only after a
sync completes end-to-end.
Drift detection
aide which shows a one-line banner under the active context when
the context is out of sync. Two cheap signals fire it:
- The context's recorded
config_hashdiffers from the currentconfig.yamlhash (someone edited the config). - The desired set computed for the context has items not yet
recorded as managed in
managed.json(sync never ran, or new declarations were added).
Both checks are in-process - no agent CLI poll, no network call. The
banner just says "config changed since last sync - run aide sync"
or "never synced - run aide sync to install declared plugins/MCP
servers".
Per-agent capability matrix
Not every agent supports every shape. The provisioner driver advertises
what it consumes via SupportedSourceShapes.
| Agent | Plugin marketplaces | URL-direct plugins | MCP servers |
|---|---|---|---|
claude | ✅ | - | ✅ (~/.claude.json) |
copilot | ✅ | - | ✅ (~/.copilot/mcp-config.json) |
codex | ✅ (via TOML edit) | - | ✅ (~/.codex/config.toml) |
gemini | - | ✅ (extensions block) | ✅ (~/.gemini/settings.json) |
cursor-agent | - (no plugin surface) | - | partial (see below) |
For cursor-agent, MCP lives at ~/.cursor/mcp.json globally and
.cursor/mcp.json per-project. The provisioner driver for cursor is
tracked separately (the agent has no plugin/marketplace CLI surface;
project-scope MCP handling lands with the project-override work).
If a context declares a shape the driver doesn't support (e.g.
URL-direct plugins for claude), aide sync --plan reports a
capability mismatch and aborts before any installation.
Profile interaction
When a context declares profile: <name>, the driver injects its
config-dir env var (CLAUDE_CONFIG_DIR=~/.claude-<name> for claude,
GEMINI_HOME=~/.gemini-<name> for gemini, etc.) into the resolved
context env. The agent CLI shelled out by aide sync then writes to
that profile-specific config dir.
This is why multi-profile contexts work end-to-end: aide sync --context work writes Claude plugins to ~/.claude-work/ while
aide sync --context oss writes to ~/.claude-oss/, even though
both contexts use agent: claude.
See Contexts for the profile field details.
Trust boundary
Provisioning operates on the global ~/.config/aide/config.yaml. The
project-scope .aide.yaml does not participate in sync today -
its plugin/MCP declarations are ignored by aide sync. This is a
deliberate trust boundary: a malicious .aide.yaml in a repo you
just cloned cannot cause aide to install agent plugins or MCP servers
without your action. Merging trusted .aide.yaml declarations into
sync is tracked separately.
Out of scope (today)
- Goose, Amp, and Aider provisioning - drivers not implemented yet.
- Project-scope
.aide.yamlplugin/MCP merging intoaide sync- separate work (trust gate). - Plugin pinning by version - sync currently installs whatever the agent's marketplace returns for a given plugin name.
- Cross-agent migration (Claude plugin → Gemini extension) - out of scope; agents have different runtime models.
Examples
Minimal: one context, three plugins
agents:
claude:
binary: claude
plugins:
jskswamy/claude-plugins: [commit-tools, craft, refactor]
contexts:
default:
agent: claude
Run aide sync once and the three plugins land in ~/.claude/.
Multi-profile: same agent, different config dirs
plugins:
jskswamy/claude-plugins: [commit-tools, craft, jot, refactor]
steveyegge/beads: [beads]
contexts:
work:
agent: claude
profile: work
# → CLAUDE_CONFIG_DIR=~/.claude-work
# `aide sync --context work` installs the four plugins there
oss:
agent: claude
profile: oss
plugins:
exclude:
- jskswamy/claude-plugins/refactor # no refactor on OSS
# → CLAUDE_CONFIG_DIR=~/.claude-oss
# `aide sync --context oss` installs three plugins there
Adoption: bring an existing setup under management
# You already installed claude plugins by hand. Run once:
aide adopt --yes
# config.yaml now lists everything that was installed.
# Subsequent syncs treat them as managed:
aide sync --plan