Skip to content
Skip to main content
The Claude, Terraform and GitHub logos on frosted tiles over a dark emerald technical background of branching dependency lines, representing managing Claude agents as code in a repository with ant apply
8 min readBy Carlos Aragon

ant apply: Manage Claude Agents as Code

ant apply is Terraform for Claude agents. You write each agent, skill, environment, memory store and scheduled deployment as a file in your repo, run one command, approve the plan it prints, and commit the claude-lock.json it writes so later runs update those same resources instead of creating new ones. It landed in ant CLI 1.30.0. If you have ever opened the Console and found four near-identical agents with no idea which one production is calling, that is the problem this solves — and the design is more opinionated than the one-line changelog entry suggests.

Your First Agent Is a Markdown File

There is no new config language to learn. An agent is a Markdown file: the frontmatter is its configuration, the body is its system prompt.

agents/summarizer.md
---
name: Summarizer
model: claude-opus-5
tools:
  - type: agent_toolset_20260401
---

You are a helpful assistant that writes concise summaries.

Then ant apply agents/summarizer.md. In a terminal it prints the plan and waits: which credentials it is using, which host, which organization, which workspace, and one line per file with its action. Answer y and it creates the agent; answer d first and you get the full field list for a create, or a field-by-field diff for an update.

Showing the organization and workspace in the plan is a small thing that prevents a large class of accident. Half the infra mistakes I have cleaned up over the years come down to somebody running the right command against the wrong account. Here it is four lines above the confirmation prompt.

--dry-runprints that detailed plan and exits, touching nothing. That is your PR preview and your "what is actually different between my file and the live agent" command.

The Lockfile Is the Whole Design

Everything interesting about ant apply is in claude-lock.json. It gets written in whatever directory you run the command from, which is the first rule: run it from the repository root, always.

claude-lock.json
{
  "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"
    }
  }
}

Two hashes, two different jobs. hash fingerprints what was last sent, so a later run notices you edited the file. remote_hash fingerprints what the API returned, so a later run notices somebody changed the agent in the Console. That is full drift detection with no state server and no daemon — just a file you already commit.

The origin block earns its place too. It pins the organization and workspace, and ant apply refuses credentials that resolve to any other organization or workspace. Your production lockfile physically cannot be applied with staging keys. Anyone who has ever fat-fingered a Terraform workspace knows exactly how much that is worth.

One practical payoff: the lockfile is where you read an agent's ID when you want to start a session from the SDK. Stop pasting agent_... strings into environment variables and read them out of the file your repo already tracks.

How Does ant apply Know What Each File Is?

When it walks a directory it decides each file's kind from the first rule that matches:

  • A top-level type field in the file.
  • The directory the file sits directly in: agents/, environments/, memory_stores/ or deployments/.
  • A file name that starts with the kind, like environment_staging.md.

Files matching none of these get skipped — your README, your CI config, your notes — unless you name one explicitly on the command line. Name a stray Markdown file and it is treated as an agent. Name a stray YAML or JSON file and you get an error instead.

Use the directory convention and never think about this again. The type field exists for the odd file that has to live somewhere else; it is not the default path.

Any resource except a skill can be YAML, JSON or Markdown. A skill is the exception: it is a directory with a SKILL.md at its root, uploaded as one bundle — the same portable format covered in Claude Skills vs MCP. In Markdown, frontmatter is the request body and the prose fills that kind's text field: an agent's system prompt, an environment's or memory store's description, a deployment's opening message.

Resources Point at Each Other by Path

This is the feature that turns a folder of files into a project. Wherever the API expects another resource's ID, you write the relative path to that resource's file instead. ant apply creates everything in dependency order and substitutes the real IDs.

deployments/nightly.md
---
name: Nightly review
agent: ../agents/reviewer.md
environment_id: ../environments/cloud.yaml
resources:
  - path: ../memory_stores/review-notes.yaml
    access: read_write
schedule:
  type: cron
  expression: "0 3 * * *"
  timezone: America/Los_Angeles
---

Review any open pull requests. Start with the oldest.

Notice the resources entry: a path can sit inside an object and the sibling keys, here access, are preserved. And references to agents and skills are pinned to the version just applied, so editing reviewer.md updates every file pointing at it in the same run. No stale version pins, no second command.

To reference something these files do not manage, write the raw ID (agent_..., skill_...). Anything else — {type: anthropic, skill_id: xlsx}, for instance — is passed through to the API as written.

The nicest touch: a skill reference can be a GitHub URL shaped like https://github.com/<owner>/<repo>/tree/<branch>/<dir>. ant apply downloads that directory, uploads it, and pins it to the resolved commit until you run --upgrade. Set GITHUB_TOKEN for a private repo. That is a real dependency system for skills, shipped quietly in a flag.

The Adoption Gap Nobody Warns You About

Here is the sentence to read twice before you plan a migration: ant apply cannot adopt a resource you created in the Console or with ant beta:agents create.

Only what is in the lockfile is managed. Write a file describing an agent that already exists, apply it, and you now have two agents — the original still running, the new one identical and unused. There is no import command, no adopt flag.

One escape hatch exists, and it is worth knowing: the Console's Export as code download ships with its own claude-lock.json, so applying that export updates the resources you built in the Console rather than cloning them. If you have real agents in the Console today, export them — do not hand-write the files and hope.

Otherwise you are choosing between a clean cutover and a duplicate you prune later. Pick deliberately, on a quiet afternoon, not halfway through a release.

Why Deleting a File Deletes Nothing

ant apply is conservative about destruction, which is correct and occasionally surprising.

If a resource was edited, archived or deleted outside these files, the plan ends with This plan cannot be applied: plus the reason, and the command exits with refusing to apply. --force overwrites the outside edit or creates a replacement. Making drift a hard stop rather than a silent overwrite is the right default.

Delete a file and the resource stays alive with a warning. --prune is what actually removes it — archiving the resource, or deleting it outright for a skill.

Which makes renaming the real footgun. Resources are keyed by path, so renaming a file declares a brand-new resource and leaves the old one running until someone prunes. A tidy-up PR that renames three agents quietly doubles your agent count, and nothing in the plan screams about it. If you rename, prune in the same PR. This is the same class of problem as untracked workflow drift in n8n without enterprise version control— the tool is only as honest as the key it tracks you by.

Field-level behaviour follows the same instinct: delete a field and it is cleared if the API allows clearing it. A field you never set, or one the API cannot clear, quietly keeps its current value. So a file is a declaration of the fields it mentions, not a complete picture of the resource.

And a bare ant apply with no arguments reconciles only the files the lockfile already tracks. At a terminal it will list untracked resource files below the lockfile and offer to add them. That offer does not exist in CI — which leads directly to the next part.

Running ant apply in CI Without Shooting Yourself

Without a terminal, ant apply prints the plan and stops. Five rules make the CI setup boring, which is what you want:

  • Apply on merge, and name the directory: ant apply --yes . on your default branch. A bare ant apply --yes reconciles only tracked files and silently skips a newly added one. That trailing dot is the difference between shipping a new agent and wondering why it never appeared.
  • Preview on pull requests: ant apply --dry-run . posts the plan for reviewers. But it exits 0 even when the plan is blocked, so do not use it as a merge gate. It informs humans; it does not enforce anything.
  • Commit the lockfile even when the job failed: a partial apply still records what it created. Skip this and your next run re-creates half of it.
  • One apply at a time: nothing locks the lockfile. Concurrency group of one, or you will race yourself.
  • Use Workload Identity Federation, not a stored API key — with an identity that reaches the exact organization and workspace recorded in the lockfile, since anything else is refused.

Pair that with hooks for the guardrails you want enforced and your agent configuration finally has the same review path as the rest of the codebase.

The Flags, in One Table

FlagWhat it does
--dry-runPrint the plan and exit. Writes nothing. Exits 0 even when blocked.
--yesApply without confirmation. Required when there is no terminal.
--forceApply over a resource changed, archived or deleted outside the files.
--pruneRemove resources in the lockfile that no longer have a file.
--upgradeRe-resolve skills referenced by GitHub URL, otherwise pinned to a commit.
--lock-fileUse a specific lockfile. Keep one per organization or workspace.
--verboseShow unchanged resources and full field values in the plan.

The full reference lives in Anthropic's ant apply docs, and there is a complete GitHub Actions example in the CLI README.

What I'd Actually Ship

Lean on the directory convention and skip type fields entirely — agents/, environments/, memory_stores/, deployments/, skills/, done. One lockfile per workspace, at the repo root, and --lock-file only if you genuinely run several.

--dry-run on pull requests, --yes on merge, and never --force in CI. Drift should cost a human thirty seconds of attention. The moment --force lives in a workflow file, the lockfile stops meaning anything and you are back to guessing what production is running.

Treat renames as deletes. Prune in the same PR, or write the rule down somewhere your team will actually see it, because the plan will not warn you.

And the honest caveat: this is beta surface. The toolset strings carry dates (agent_toolset_20260401), the lockfile is at version: 1, and field names will move. Check every string in this post against the docs before you paste it. The shape — declare, plan, apply, lock, review — is the part that will still be here next year.

Do You Know Which Agent Production Is Calling?

Most teams I audit have agents living in a Console, prompts nobody can diff, and an ID pasted into an environment variable two quarters ago. I'll get yours into a repo, wire the apply into CI, and leave you with a review loop that survives the next person who joins.

Related Posts