cmaform
v0.2.0
Published
Terraform-style declarative management for Anthropic Managed Agents, Skills, Memory Stores, Environments, and Vaults.
Maintainers
Readme
cmaform is a Terraform-style CLI for managing Anthropic Managed Agents, Skills, Memory Stores, Environments, and Vaults as files in your repo. You declare each resource as a YAML file, run cmaform plan to see what will change, and cmaform apply to ship it.
[~] update agent "release-prep" (id=agent_01Qx..., version=6)
file: agents/release-prep.yaml
~ system:
... (12 unchanged lines)
- prior wording...
+ revised wording...
... (8 unchanged lines)
[+] create skill "spec-lookup"
dir: skills/spec-lookup
hash: 7b8b14094e01...
Plan (agents): 0 to add, 1 to change, 0 to archive, 5 unchanged.
Plan (skills): 1 to add, 0 to change, 0 to delete, 0 unchanged.
Plan (memory_stores): 0 to add, 0 to change, 0 to archive, 0 unchanged.
Plan (environments): 0 to add, 0 to change, 0 to archive, 1 unchanged.
Plan (vaults): 0 to archive, 1 unchanged.⚡ Quick Start
Try it first
npx cmaform --helpInstall and use
npm install -g cmaformBootstrap a new config directory and import an existing managed agent
export ANTHROPIC_API_KEY=sk-ant-...
mkdir my-agents && cd my-agents
cmaform pull agent_011CaSWcCrMdQdp4SA6TVdH6 # writes agents/<name>.yaml + state
cmaform plan # show the diff
cmaform apply # confirm → push to AnthropicAnthropic's Managed Agent / Skills / Memory Stores / Environments / Vaults APIs are currently beta. cmaform calls
@anthropic-ai/sdk'sbeta.agents.*/beta.skills.*/beta.memoryStores.*/beta.environments.*/beta.vaults.*endpoints directly.
🚀 Usage
Commands
| Command | What it does |
| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| cmaform pull <id> [--by-id] | Import a remote resource by ID (agent_* / skill_* / memstore_* / env_* / vlt_*) into local files + state. Pass --by-id to keep raw IDs in the written agent YAML instead of rewriting multiagent.agents[] / skills[] to the name form |
| cmaform plan [--verbose\|-v] [target...] | Diff local YAML / state / remote and print a Terraform-style plan |
| cmaform apply [--yes\|-y] [--verbose\|-v] [target...] | Show plan, prompt for confirmation, apply, save state |
| cmaform sync [--by-id] | Re-fetch every entry in state from remote and rewrite local YAML. Pass --by-id to keep raw IDs in the written agent YAML instead of rewriting refs to the name form |
| cmaform init | Initialize / reconcile the state file against remote (no remote writes; spirit of terraform init) |
| cmaform list | Show local files / state / remote side-by-side |
| cmaform fmt | Rewrite multiagent.agents[].id / skills[].skill_id in local YAML to the name form, using cmaform.state.json for the id → name lookup |
Filtering plan / apply
The last positional arguments to plan and apply are targets. A target can be a resource kind or an individual resource name — useful for staged rollouts (e.g. create a skill first, copy its skill_id into an agent YAML, then apply the agent).
cmaform apply skills # all skills
cmaform apply agents # all agents
cmaform apply slack-mention-lookup # a single skill by name
cmaform apply release-prep --yes # a single agent, skip confirmation
cmaform apply skills release-prep # all skills + one agentA <target> is either a kind alias (matches every resource of that type) or the individual name of one specific resource. Use the table below for kind aliases; anything else is treated as a resource name.
| Resource | Kind aliases | Individual name format | Example |
| ------------ | ----------------------------------------------------------- | ------------------------------------- | ------------------------------------------------------- |
| Agent | agent / agents | name: field in the YAML | release-prep (agents/release-prep.yaml) |
| Skill | skill / skills | directory name under skills/ | slack-mention-lookup (skills/slack-mention-lookup/) |
| Memory Store | memory_store / memory_stores / memstore / memstores | directory name under memory_stores/ | team-notes (memory_stores/team-notes/) |
| Environment | environment / environments / env / envs | directory name under environments/ | python-dev (environments/python-dev/) |
| Vault | vault / vaults | directory name under vaults/ | my-bot (vaults/my-bot/) |
plan expands create / update diffs symmetrically: a new resource is rendered as + field: ... blocks just like an update is rendered as ~ field: ... blocks. Long string fields (system, description) are truncated to 3 lines plus an ... (N lines hidden) marker. Pass --verbose to show full content.
If you pass an individual name that doesn't exist anywhere (local YAML, state, or remote), cmaform exits with code 2. An unmatched kind is fine — it just shows 0 to add, 0 to change, ....
Pulling existing resources
cmaform pull agent_011CaSWcCrMdQdp4SA6TVdH6 # writes agents/<name>.yaml
cmaform pull skill_013uPS15B3Kw82NpjH4uNQep # state only — SKILL.md is not regenerated
cmaform pull memstore_01ABC... # writes memory_stores/<name>/manifest.yaml
cmaform pull env_015G... # writes environments/<name>/manifest.yaml
cmaform pull vlt_011CaQ... # writes vaults/<name>/manifest.yaml (credentials not managed yet)Skill content is not returned by the Anthropic API once uploaded, so pull for skills only records the ID + version + display title into state. The local SKILL.md must be authored by you.
📦 Resources
cmaform manages five resource types — agents, skills, memory stores, environments, and vaults. Each lives under its own top-level directory beneath the config root. (Create vaults with cmaform, then finish configuring them on the Anthropic Console.)
Agent (agents/<name>.yaml)
name: my-agent # unique within the workspace — this is the identity key
model:
id: claude-sonnet-4-6
speed: standard # standard | fast
description: short description
system: |-
... full system prompt ...
mcp_servers:
- name: slack
type: url
url: https://mcp.slack.com/mcp
tools:
- type: agent_toolset_20260401
default_config:
enabled: true
permission_policy:
type: always_allow
configs: []
skills: []
metadata: {}- Identity: the
namefield.agent_idlives in state, not YAML. - Diff: deep-equal on
name/model/description/system/tools/mcp_servers/skills/multiagent/metadata. - Delete: present in state but missing locally ⇒
archive(reversible). - Arrays (
tools,mcp_servers,skills) are fully replaced — local is the source of truth.
See the Anthropic Agent Setup docs for the full schema.
Skill (skills/<localName>/)
Each skill is a directory. The Anthropic API uploads a folder containing SKILL.md plus any auxiliary files.
skills/<localName>/
├── SKILL.md # required (YAML frontmatter + markdown body)
├── REFERENCE.md # optional
└── scripts/
└── helper.py # optionalSKILL.md requires frontmatter:
---
name: my-skill
description: What this skill does, and when Claude should use it.
---
# My Skill
...name: ≤64 chars,[a-z0-9-]only.anthropic/claudeare reserved.description: ≤1024 chars.
cmaform computes a SHA-256 hash of the entire directory and compares it against the hash stored in state. If they differ, apply uploads a new version of the skill.
Referencing a skill from an agent
# agents/foo.yaml
skills:
- type: anthropic
skill_id: xlsx # well-known Anthropic skills stay id-based
- type: custom
name: slack-mention-lookup # = local directory name under skills/
version: latest # optionalThe raw-ID form (skill_id: skill_01XXXXXX) is also accepted for type: custom — name and skill_id are interchangeable. See Name-based references below for resolution semantics.
⚠️ Skills cannot be archived. Deleting the directory and running
applypermanently deletes the skill and all of its versions.
🔗 Name-based references
multiagent.agents[] and skills[] accept references by logical name in addition to raw IDs. Names are resolved at plan / apply time, so YAML can reference resources without committing any workspace-specific IDs.
# agents/coordinator.yaml
multiagent:
type: coordinator
agents:
- type: agent
name: spec-qa # = the `name` field of agents/spec-qa.yaml
- type: agent
name: release-prep
version: latest # optional
skills:
- type: custom
name: slack-mention-lookup # = the directory name under skills/Resolution order for each name:
cmaform.state.json— if the resource is already tracked locally, use its ID.- Remote —
findAgentByName/findSkillByDisplayTitle(cached per run). - Local apply set — if a YAML for that name exists locally and is being created in this run, the reference is treated as a forward dependency. cmaform substitutes a placeholder during plan and replaces it with the real ID right after the dependency is created.
If none of the above match, plan / apply fails with an error pointing at the unresolved name.
pull / sync write back in name form
When writing remote agents back to YAML, cmaform replaces any id / skill_id that resolves to a known local name with the name form. IDs for resources not tracked in state are kept as-is.
Pass --by-id to pull / sync to opt out of the rewrite and keep raw IDs in the written YAML.
Raw-ID form
{ type: agent, id: agent_... } and { type: custom, skill_id: skill_... } are also accepted and behave identically to the name form.
If an entry writes both name: and id: (or skill_id:), cmaform resolves the name and asserts that it matches the pinned ID. Mismatches abort plan / apply with a clear error. This is useful as a safety net while migrating from id-based references — cmaform fmt can then drop the redundant raw IDs once everything verifies.
type: anthropic skills (well-known IDs like xlsx) always use the skill_id form. type: self agent references take neither.
Memory Store (memory_stores/<localName>/manifest.yaml)
name: my-store
description: optional
metadata:
team: platform- Diff fields:
name/description/metadata. metadatais patched: keys missing locally are deleted, others are upserted.- Delete =
archive(one-way; the store's memory data is preserved).
Environment (environments/<localName>/manifest.yaml)
name: python-dev
description: optional
config:
type: cloud
packages:
pip:
- pandas
- numpy==2.2.0
npm:
- express
networking:
type: limited
allowed_hosts:
- https://api.example.com
allow_mcp_servers: true
allow_package_managers: true
metadata: {}- Diff fields:
name/description/metadata/config. - Only
config.type: cloudis currently supported (self-hosted environments are out of scope). - Empty package lists,
falsedefaults in limited networking, and the server-sidetype: 'packages'marker are normalized away so the plan is idempotent. - Delete =
archive. Archived environments stop accepting new sessions but existing sessions keep working.
Vault (vaults/<localName>/)
Status: ⚠️ Partial support — vault design is still evolving in cmaform.
| Operation | Supported by cmaform | Notes | | ------------------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Create vault | ✅ | New local manifest → vault is created on the server. | | Archive vault | ✅ | Removing a local manifest archives the vault. Cascades server-side to attached credentials. | | Update vault | ❌ | Edits to
display_name/metadataafter creation are silently ignored. Archive and recreate to rename / relabel. | | Manage credentials | ❌ | Credentials are not yet managed by cmaform — the secret-resolution design is still being settled. Use the Anthropic Vault Credentials API directly to attach / rotate credentials. |The vault scope will be expanded (update, credential management with secret backends) in a future release.
A vault is a container that holds credentials for MCP servers. The local layout is intentionally minimal in this release:
vaults/
└── my-bot/
└── manifest.yamlmanifest.yaml — the vault definition:
display_name: my-bot
metadata:
external_user_id: bot🗂️ Directory Layout
cmaform reads from the current working directory (or CMAFORM_DIR if set):
<cwd>/
├── agents/
│ └── *.yaml
├── skills/
│ └── <localName>/SKILL.md
├── memory_stores/
│ └── <localName>/manifest.yaml
├── environments/
│ └── <localName>/manifest.yaml
├── vaults/
│ └── <localName>/manifest.yaml
└── cmaform.state.json🧾 State File (cmaform.state.json)
{
"agents": {
"release-prep": { "id": "agent_01Qx...", "version": 6 }
},
"skills": {
"slack-mention-lookup": {
"id": "skill_013uPS...",
"version": "1778647403232223",
"hash": "7b8b14094e01...",
"display_title": "slack-mention-lookup"
}
},
"memory_stores": {
"team-notes": { "id": "memstore_01...", "name": "team-notes" }
},
"environments": {
"python-dev": { "id": "env_01...", "name": "python-dev" }
},
"vaults": {
"my-bot": { "id": "vlt_01...", "display_name": "my-bot" }
}
}- Maintained by
pull/apply/sync/init. - Recommended to
.gitignore(treat as the local source of truth, like a Terraform state file). - If someone shares a state file with you, run
cmaform syncto regenerate the local YAML for every agent (skill bodies cannot be restored — see above).
🔐 Environment Variables
| Variable | Required | Purpose |
| ------------------- | -------- | -------------------------------------- |
| ANTHROPIC_API_KEY | ✅ | Anthropic API authentication |
| CMAFORM_DIR | | Config root directory (default: cwd) |
⚠️ Caveats
- Renaming an agent creates a new agent. To rename, delete the old YAML, run
apply(archives the old one), then add the new YAML with the new name. - Deleting a skill directory and running
applyis destructive — there is no archive for skills. - For full reproducibility, avoid making manual changes in the Anthropic Console. Always go through the YAML /
SKILL.md/manifest.yaml.
🛠️ Development
mise install
pnpm install
pnpm dev -- --help # run from source (tsx)
pnpm typecheck
pnpm build # bundle to dist/cli.js via tsup
node dist/cli.js --help📋 Requirements
- Node.js ≥ 22
- An Anthropic API key with access to the Managed Agent / Skills / Memory Stores / Environments / Vaults beta
📄 License
MIT
