The automation that keeps my sites moving is split in two. The run configuration lives in the Console; the skills live in the repository. Every session used to start with me remembering which side was authoritative, and a few minutes of every morning went there.
ant apply, added in ant CLI v1.30.0, looks like it dissolves that split. You describe agents, environments, skills, memory stores, and deployments as files in your repository, run the command, read the plan it prints, and approve it. The configuration then lives in git history — and the remembering stops.
Before moving anything, though, I paused on one question. What happens to the resources I already built in the Console? Read that wrong and the same agent quietly becomes two.
Start by reading --dry-run
Don't apply first. Read the plan first.
# Print the detailed plan and exit without changing anything
ant apply --dry-run .In an interactive terminal, ant apply prints the creates and updates and waits for approval. Answer d and you get the fields of each new resource, or a field-by-field diff for each update. --dry-run prints that same detail and stops. It writes no lockfile.
One caveat is worth pinning down. --dry-run exits 0 even when the plan is blocked. If you wire it into CI as the gate that decides whether a plan is acceptable, it will report a blocked plan as a clean one. Treat it as information for reviewers, nothing more.
Refusal only covers what the lockfile knows
This is the part I misread first.
If a resource that came from your files is later edited, archived, or deleted somewhere else — in the Console, say — the next ant apply ends with This plan cannot be applied: and the reason, then exits with refusing to apply. You pass --force only when you actually want to overwrite that change. The CLI tells you when your declarations and reality have drifted apart.
But write a file describing a resource you created in the Console, apply it, and you get a second resource. ant apply cannot adopt existing resources. Only what the lockfile tracks is managed; everything else reads as a declaration of something that does not exist yet.
Refusal happens inside the lockfile. Outside it, things are duplicated without complaint. Swallow that sentence early and the migration order takes care of itself.
There is a way through. The Console's Export as code download ships with its own claude-lock.json. Apply that, and you are updating the resources you assembled in the Console rather than creating rivals for them. Without knowing this, I would have parked a same-named agent that does nothing right next to a deployment that was actually running.
claude-lock.json is the record, not an artifact
The first apply writes claude-lock.json at the directory you ran it from. It looks roughly like this.
{
"version": 1,
"origin": {
"base_url": "https://api.anthropic.com",
"organization_id": "YOUR_ORGANIZATION_ID",
"workspace_id": "YOUR_WORKSPACE_ID"
},
"resources": {
"./agents/release-notes.md": {
"kind": "agent",
"id": "agent_XXXXXXXXXXXXXXXX",
"version": "1",
"hash": "…",
"remote_hash": "…"
}
}
}hash fingerprints what was last sent; remote_hash fingerprints what the API returned. Together they let a later run tell an edited file apart from a resource that was changed outside your files.
Three operational decisions follow from that.
Run it from the repository root, so the lockfile lands in one predictable place. Commit the lockfile even when the apply failed partway, because a partial apply still records what it managed to create. And if you work across organizations or workspaces, keep one lockfile each and name it with --lock-file; ant apply refuses credentials that resolve to an organization or workspace other than the one recorded there.
Five doors a second copy walks through
| Door | What happens | What to do |
|---|---|---|
| Rewriting a Console resource as a file | It is not adopted — a second one is created | Bring the Export as code download with its claude-lock.json |
| Renaming or moving a file | It declares a new resource; the old one stays | Restore the name, or clear the old one with --prune |
| Deleting a file | The resource remains, with a warning | --prune (deletes a skill, archives everything else) |
Bare ant apply --yes in CI | Reconciles only tracked files and skips newly added ones | Name the directory: ant apply --yes . |
Forgetting to commit claude-lock.json | The next run cannot find the resources and rebuilds them | Commit it alongside the files |
Four of those five happen to people who wrote their files carefully. The cause sits not in the description but in where the identity of the resource is recorded.
Cross-references are relative paths, not IDs
Wherever the API expects another resource's ID, write the relative path to that resource's file. ant apply creates things in dependency order and fills in the real IDs.
---
name: Release notes writer
model: claude-opus-5
tools:
- type: agent_toolset_20260401
skills:
- ../skills/release-notes
---
You draft release notes. Summarize what changed in three short sections.Deployments follow the same shape: frontmatter is the request body, and the prose becomes the message that starts each session.
---
name: Nightly release notes
agent: ../agents/release-notes.md
environment_id: ../environments/cloud.yaml
schedule:
type: cron
expression: "0 3 * * *"
timezone: Asia/Tokyo
---
Draft release notes from the commits that have not been written up yet.Paths are what let a single run pin the referring side to the version it just applied. Hard-code an ID and that follow-through stops. Reach for a literal agent_... or skill_... ID only when you are pointing at something these files do not manage.
A file's kind is inferred in order: a top-level type field, then the directory it sits directly in (agents/, environments/, memory_stores/, deployments/), then a filename that starts with the kind, such as environment_staging.md. Files matching none of these are skipped during a directory walk — which keeps READMEs and CI configuration out, and also means that the moment you tidy a file out of agents/, it stops being seen.
Write the local run and the CI run as separate procedures
With no terminal, ant apply prints the plan and stops rather than asking for confirmation. So CI looks like this.
# On pull requests: print the plan for reviewers (informational, always exits 0)
ant apply --dry-run .
# On the default branch after merge: name the directory and apply
ant apply --yes .Run one apply at a time, since nothing locks the lockfile. And authenticate with Workload Identity Federation rather than a stored API key, as an identity that reaches the organization and workspace the lockfile records.
I keep a number of unattended jobs running, and the pain in setups like this almost always arrives the same way: it worked on my machine and did something else in CI. Keep the one-off local run and the repeated CI run as two separate write-ups. It looks like extra work and it prevents more accidents than anything else I've tried.
The one file to start with
You don't have to move everything at once. Take the agent you touch least often, write it into a single file, and stop at reading the plan from ant apply --dry-run. If the plan says create, that is a new resource, not an adoption — and that is the moment to decide between starting from Export as code and standing up a fresh one.
I'm still mid-migration myself, with pieces left in the Console. If you're hesitating at the same spot, I hope this saves you the duplicate.