
Version 1.30.0 of the ant CLI, shipped 3 September, adds ant apply. It reads agents, environments, skills, memory stores and scheduled deployments out of files in your repo, prints a plan, and writes a claude-lock.json you are told to commit.
The lockfile keys every resource by its file path. Rename a file and you have declared a new agent while the old one keeps running. Delete a file and the resource stays live, because removal is opt-in behind --prune.
It cannot adopt anything you already built. Applying a file that describes an existing Console agent creates a second one, and a duplicated scheduled deployment carries a per-run budget rather than a total, so the second copy bills separately every time the cron fires.
Anthropic shipped a lockfile for agents on 3 September. Version 1.30.0 of the ant CLI adds ant apply, which reads agents, environments, skills, memory stores and scheduled deployments out of files in your repository, prints a plan, waits for approval, and reconciles them against the Claude API. It writes claude-lock.json and tells you to commit it alongside the files.
Most of the coverage called it Terraform for agents and stopped there. The comparison is close enough to be useful, and wrong in the one place that costs money.
The primary key is the path
The lockfile after applying a single agent looks like this:
{
"version": 1,
"origin": {
"base_url": "https://api.anthropic.com",
"organization_id": "1b0c2a4d-...",
"workspace_id": "wrkspc_01JwQvzr7rXLA5AGx3HKfFUJ"
},
"resources": {
"./agents/summarizer.md": {
"kind": "agent",
"id": "agent_011CYm1BLqPXpQRk5khsSXrs",
"version": "1",
"hash": "d23251c8d99b3613a64f3f8d87f5fad4",
"remote_hash": "1b771bee5bdbf600a5ad972fdac32d94"
}
}
}
Read the key. Not the agent name, not a logical identifier you chose, not a hash of the content. The relative path of the file on disk is the thing that binds your declaration to agent_011CYm1BLqPXpQRk5khsSXrs on Anthropic's side. The two hashes underneath it fingerprint what the CLI last sent and what the API last returned, which is how a later run notices that you edited the file or that somebody edited the resource in the Console.
Path as primary key is a defensible choice for a version 1. It is also the source of every sharp edge in the tool.
Rename agents/reviewer.md to agents/code-reviewer.md and the plan shows a create, because the new path is not in the lockfile. The agent that used to be called Code reviewer is still there, still deployed, still referenced by anything holding its ID. The docs are direct about it: renaming a file declares a new resource and leaves the old one in place until you prune. A refactor that any engineer would consider cosmetic doubles the object it names.
Cross-file references work the same way. A coordinator agent lists ./reviewer.md in its roster, a deployment names its agent as ../agents/reviewer.md, and ant apply resolves those to real IDs in dependency order at apply time. Move a directory and you are rewriting the graph, not the layout.
It will not adopt what you already have
This is the part the Terraform framing hides. Terraform has import. ant apply does not.
The docs state it plainly: ant apply cannot adopt a resource created in the Console or with ant beta:agents create. Only what is in the lockfile is managed, and applying a file that describes an existing agent creates a second one.
So the migration path for a team that has been building Managed Agents since April is not a migration path. Every agent, environment and scheduled deployment made by hand is invisible to the reconciler. Writing a file that faithfully describes the agent you already run does not bring it under management. It duplicates it, and now you have two agents with the same name, the same system prompt, and different IDs, one of which is in Git and one of which is not.
There is one escape hatch. Exporting an agent from the Console with Export as code produces a download that includes its own claude-lock.json, so applying that updates the resource you built there instead of cloning it. That works per agent, from the UI, by hand. It is not a bulk import, and it is the only route in.
The drift runs one direction
The safety defaults are asymmetric, and they lean toward accumulation.
If a resource was edited, archived or deleted outside your files, the plan refuses to apply and tells you why. Good. That is the conservative choice, and --force is there when you mean it.
If a file disappears from your repo, the resource stays running. You get a warning. --prune is what actually archives it, and it is a flag you have to remember on a command you probably run from CI with --yes. Creation is automatic and destruction is opt-in, which means the set of live resources only ever grows unless somebody is watching.
Then there is this line in the CI guidance: run one apply at a time, because nothing locks the lockfile. Two merges to your default branch landing close together, two jobs racing, and the file that exists to prevent duplicate resources has no concurrency control of its own. Anthropic also tells you to commit the updated lockfile even when the apply step failed partway, because a partial apply still recorded what it created. Both of those are honest disclosures. Both of them are also the sound of a state file that is younger than the thing it tracks.
What the extra copy actually costs
A duplicated agent sitting idle is a nuisance. A duplicated scheduled deployment is a bill.
Deployments run sessions on a POSIX cron expression with an IANA timezone, at minute granularity, with up to 15% jitter applied to spread load. An organization can hold up to 1,000 of them. They carry an optional budget, and the shape of that budget is the detail worth internalizing: the deployment copies the cap onto each session it starts, so it bounds every run separately rather than acting as a cumulative ceiling. A cap of "2000" means roughly twenty dollars on every run, not twenty dollars total.
Put those together. A rename in a pull request creates a second nightly deployment. The original is not archived, because nobody passed --prune. Both fire at 3am, both open a session against the same memory store with read_write access, and both spend up to their cap. The per-run budget that protects you from one runaway session does nothing about two deployments doing the same work twice, and the deployment run records look healthy in both, because each one succeeded.
The failure modes that do get caught are the loud ones. An archived agent auto-archives its deployment. An archived subagent, environment or vault records a failed run and pauses the deployment, with paused_reason.error.type mirroring the run error. Anthropic handles the broken cases well. The expensive case is the working one, run twice.
There is a smaller version of the same trap in the DST semantics. Cron matches literal wall-clock time, so a schedule in the 1am to 3am window fires twice on a fall-back day and not at all on a spring-forward day. The docs say to schedule outside that window or use UTC. Worth doing before you have a hundred of these.
Where this actually lands
The agent loop has been moving out of the codebase and onto vendor infrastructure for most of this year. ant apply is the counter-move: the definition comes back into your repo, gets reviewed in a pull request, and ships through CI. The running thing still lives on Anthropic's side, and claude-lock.json is now the only thread connecting the two.
That thread is a JSON file keyed on paths, with no import, no locking, and no destruction by default. Treat it accordingly.
Three things to set up on day one. Authenticate CI with Workload Identity Federation rather than a stored key, since ant apply refuses credentials resolving to a different organization or workspace than the lockfile records, and that check is the only thing standing between a misconfigured job and a parallel copy of your entire agent estate in the wrong workspace. Keep one lockfile per workspace and point at it explicitly with --lock-file rather than letting the CLI search upward. And put a rule in code review that a moved or renamed file under agents/, deployments/ or environments/ needs a --prune in the same change, because the tool will not tell you twice.