‹ Blog

How an AI agent fixed a Santa rule on our Macs, with Terraform and code review

This morning, Claude Code, an AI agent, fixed a Santa file access rule on our Macs. It found the cause, wrote the fix as a new profile version in Terraform, opened the pull request, read the plan from CI, and checked the result on the Mac. People kept the decisions: I approved the plan, a colleague approved and merged the pull request, and CI applied it. From the first message to the fix confirmed on the device: 8 minutes and 20 seconds, while I went on with my todo list.

It started with a warning from santactl doctor on my Mac:

FileAccessPolicy rule 'SSH folder' is invalid: Rule name must match regular expression "^[A-Za-z0-9._:-]+\z"

Santa is North Pole Security’s binary authorization tool for macOS. Besides deciding which apps can run, it can control access to files: a file access (FAA) rule lists the processes that can open some files, and blocks or logs the others.

I gave this line to Claude Code, in the repository that holds the Zentral configuration of our own Macs. At no point did I write Terraform. I did not open the Zentral console either, except to speed up the recovery with a check-in. What I did:

  • I read the plan and said yes.
  • I reviewed the pull request. A colleague reviewed it too, approved it, and merged it.
  • I triggered a check-in on my Mac in the Zentral console, so it got the fix sooner.

The agent did the rest: it found the file, understood how our Zentral artifacts and profiles are versioned, made the new version, checked it, opened the pull request, read the Terraform plan from CI, watched the apply, and confirmed the result on the device.

This post goes through the session with the real transcript. Only the serial number and the Zentral hostname are changed, and some long answers are shortened ([…]). There are three kinds of boxes:

  • Admin what I typed.
  • Claude Code what the agent answered me.
  • Claude Code · tool call what the agent did between my messages: a command it ran, or a file it read, with what it got back.

The text between the boxes is my comments, written afterwards, to explain why each step worked.

The setup: our Mac configuration is code

All the configuration of our Macs, Santa included, is in a Git repository, applied with the Zentral Terraform Provider. We explained why we chose Terraform in an earlier post. For this story, three things matter:

  • Each configuration profile is a file in the repository. A Zentral MDM artifact (zentral_mdm_artifact, for example “Santa - Default configuration”) has one or more versions. Each version is a zentral_mdm_profile resource that points to a .mobileconfig file. To change a profile, you add a new version. This is how the Zentral MDM works: a version is never edited, so we always know which version each device has, and exactly what that version contains. The old versions stay in the repository.
  • CI runs terraform plan on every pull request and posts it as a comment. After the merge, CI runs terraform apply. It authenticates to Zentral with an OIDC token, so there is no long-lived secret in the pipeline.
  • main is protected. A change needs an approving review before it can be merged.

This GitOps setup is what makes the work possible for an agent. Everything it needs to understand the system is text it can read: the profiles, the Terraform resources, the history of the previous changes, and the CI workflow. And everything it changes goes through the same review as a change by a person.

What the agent was allowed to do

Claude Code ran on my Mac, in the repository, and asked before each step that changed something. It could:

  • read and change the files in the repository, and run git, plutil and Terraform, in a container;
  • push a branch and open a pull request with gh, and read the CI logs;
  • run santactl status on my Mac.

It had no part in the apply: CI applies the merged change to Zentral, with its own OIDC token. It could not run santactl doctor, which needs root. And it could not merge on its own, with one exception, which comes up below.

The warning: Santa skipped one file access rule

Admin

fix the santa confi profile, santactl doctor shows me: """FileAccessPolicy rule 'SSH folder' is invalid: Rule name must match regular expression "^[A-Za-z0-9._:-]+\z"""", make a plan

There is a typo, and it does not matter. What matters is the end of the message: make a plan. I did not ask for a fix, I asked for a plan. The agent investigates and proposes, I read the proposal, and nothing changes in the repository until I say yes. It is what you would ask a new colleague too: look at the problem, tell me what you want to do, then do it.

The agent searched the repository for the rule name and found it in two Santa profiles:

Claude Code · tool call Searched the repository
mobileconfigs/santa.default-configuration.v21.mobileconfig:274:            <key>SSH folder</key>
mobileconfigs/santa.default-configuration.v20.mobileconfig:274:            <key>SSH folder</key>

Then it read the Terraform side, to see how these files are used, and the Git history of the last changes to this profile:

Claude Code · tool call Read mdm_artifacts.tf and the Git history
resource "zentral_mdm_profile" "santa-default-configuration-21" {
  artifact_id = zentral_mdm_artifact.santa-default-configuration.id
  source = base64encode(
    templatefile(
      "${path.module}/mobileconfigs/santa.default-configuration.v21.mobileconfig",
      {
        fqdn       = var.fqdn,
        secret     = zentral_santa_enrollment.default.secret,
        realm_uuid = data.zentral_realm.default-all.uuid,
      }
    )
  )
  macos   = true
  version = 21
}
250f4ec Fix path for `.ssh` folder Santa FAA rule
2996798 Fix FAA policy
fc5b1b9 Add FAA audit only rule for `.ssh` folders

From the diff between v20 and v21, it learned our convention: a new version is a copy of the previous file, with a new PayloadUUID for the payload and for the profile, and a matching zentral_mdm_profile resource. No one told it. The repository showed it.

It did not stop at the first match. It parsed the FileAccessPolicy of every Santa profile in the repository and tested each rule name against Santa’s regular expression. Only SSH folder failed. Then it asked Santa on the Mac what it had actually loaded:

Claude Code · tool call santactl status
>>> Watch Items
  Enabled                                  | Yes
  Data Source                              | Embedded in configuration profile
  Policy Version                           | v21
  Rule Count                               | 5

This is the important finding. The profile has six rules, Santa loaded five. Santa did not reject the whole policy: it skipped the invalid rule and kept the others. So the cookie, sudoers, Spotlight and keychain rules were working, but the audit rule for the .ssh folders was not running. Santa wrote an error to its log and moved on. Unless you read that log or run santactl doctor, the only sign is a rule count one short in santactl status.

The plan: a new profile version

Claude Code

Santa accepts only letters, digits and . _ : - in a rule name, so the space in SSH folder makes that rule invalid. […]

  1. New profile v22. I copy v21 to mobileconfigs/santa.default-configuration.v22.mobileconfig, then in the copy:
    • rename SSH folder to SSHFolder, in the same style as the other rules (GoogleChromeCookies, Keychains, …);
    • change FileAccessPolicy → Version from v21 to v22, so santactl status shows which policy is loaded;
    • give the payload and the profile new PayloadUUIDs, as was done from v20 to v21.
  2. Terraform. I add zentral_mdm_profile.santa-default-configuration-22 after the v21 resource […]. The default and Entra blueprints already use this artifact, so they need no change.
  3. Checks before a PR: plutil -lint on the new file; check every rule name against Santa’s regex again; terraform fmt/validate in the hashicorp/terraform container.
  4. Check on a Mac after the apply: santactl status should show Policy Version v22 and Rule Count 6 […].

Events from this rule will now carry the name SSHFolder. No probe or query in this repo uses the old name, so nothing else needs to change.

Two details in this plan are worth a comment.

The new name follows the style of the five other rules. The agent did not only make the name valid, it made it consistent.

It also bumped the Version key of the policy. Santa does not need it, but this value is what santactl status shows. Without it, we could not tell from the device if the new profile was installed. The agent planned how to verify the change before making it.

The change: four lines in a new profile

Admin

Yes, make v22

Claude Code · tool call Diff between v21 and v22
30c30
<           <string>v21</string>
---
>           <string>v22</string>
274c274
<             <key>SSH folder</key>
---
>             <key>SSHFolder</key>
315c315
<         <string>F5B82F87-61B9-4EC5-BC14-3E064E6E93DB</string>
---
>         <string>B4689897-841C-4989-9498-D95CEAD1DC41</string>
335c335
<     <string>1C0F6AE9-8424-44E5-8D08-411250C9BD1C</string>
---
>     <string>A9AF7D2F-58FA-43BA-AB0B-FF47D17718AE</string>

Four lines, and nothing else. The new UUIDs come from uuidgen, not from the model’s imagination. A UUID copied from the previous version is a classic mistake with configuration profiles, and it is easy to miss in a review.

It added the santa-default-configuration-22 resource, and ran the checks:

Claude Code · tool call Checks
mobileconfigs/santa.default-configuration.v22.mobileconfig: OK
v22 6 ['GoogleChromeCookies', 'SlackCookies', 'Sudoers', 'SpotlightImporterProtection', 'Keychains', 'SSHFolder'] INVALID: []
fmt-ok
Success! The configuration is valid.

Terraform runs in the official hashicorp/terraform container here: a note from an earlier session tells the agent to run it that way on this Mac. Then I clicked “Create PR”. The agent made a branch, wrote the commit message, pushed, and opened the pull request with the why, the what, and how to verify it.

The Terraform plan from CI shows a drift

Admin

verify plan on ci

The agent read the log of the “Terraform Plan” job:

Claude Code · tool call Terraform plan, from CI
  # zentral_mdm_profile.santa-default-configuration-22 will be created
  + resource "zentral_mdm_profile" "santa-default-configuration-22" {
      + artifact_id        = "df707784-ff20-4475-92cc-e8e76c8935f7"
      + default_shard      = 100
      + excluded_tag_ids   = []
      + macos              = true
      + shard_modulo       = 100
      + source             = (sensitive value)
      + tag_shards         = []
      + version            = 22
      […]
    }

  # zentral_santa_scoped_client_mode.default-monitor will be updated in-place
  ~ resource "zentral_santa_scoped_client_mode" "default-monitor" {
        id                      = 1
        name                    = "Monitor"
      ~ serial_numbers          = [
          - "XYZ0123456ZZZ",
          + "XYZ0123456",
        ]
        # (11 unchanged attributes hidden)
    }

Plan: 1 to add, 1 to change, 0 to destroy.

The first resource is the expected one. The second one is not part of the change at all.

Claude Code

The CI plan passes and adds the v22 profile, but it also changes one setting that this PR didn’t touch. […]

On the server, the Monitor scoped client mode has the serial XYZ0123456ZZZ. In the repo, santa_configurations.tf:19 has XYZ0123456. So someone changed the serial on the server, outside Terraform, and the apply after the merge would set it back to XYZ0123456.

XYZ0123456ZZZ looks like someone added “ZZZ” on purpose to take that Mac out of Monitor mode.

Guilty. My Mac is a test device, and some time ago I added “ZZZ” to its serial number in the Zentral console, to take it out of the Monitor scope for a test. This scope is a zentral_santa_scoped_client_mode resource, so Terraform owns its serial numbers. Then I forgot about it. Oops.

This is the other half of configuration as code: drift detection comes for free for everything Terraform manages. Every plan compares the repository with the server, so a manual change to a managed resource cannot hide for long. The agent did not ignore the line because it was not “its” change, and it did not decide alone what to do with it. It explained it and asked.

Admin

Yes, set it back

The review rule applies to the agent too

Claude Code

I couldn’t merge the PR: branch protection on main needs an approving review, and the PR doesn’t have one. […]

  • Normal: a teammate approves the PR, then it gets merged, by you or by me.
  • Admin merge: I run gh pr merge 286 --squash --admin, which skips the review rule. I’ll only do that if you say so.

My “yes” was about the drift, not about skipping the review, and the agent read it that way. It named the admin merge as an option, and left it alone.

That option was there because the agent works with my GitHub login, and I am an admin of this repository. Here, the rule held because the agent asked first. To make it hold whatever the agent decides, give it a token that cannot bypass branch protection. The review rule is there for every change, from a person or from an agent.

A colleague reviewed the pull request, approved it, and merged it.

Checking the fix on the Mac

Admin

squashed and merged

The agent confirmed the merge, found the “Terraform Plan/Apply” run on main, and waited for it:

Claude Code · tool call Apply, from CI
Plan: 1 to add, 1 to change, 0 to destroy.
Apply complete! Resources: 1 added, 1 changed, 0 destroyed.

The “1 changed” is the drift explained in The Terraform plan from CI shows a drift.

Then it checked the Mac again, and did not claim a success it could not see:

Claude Code

This Mac hasn’t installed v22 yet. santactl status still shows Policy Version v21 with Rule Count 5, so the fix isn’t confirmed on a device. Once the MDM installs the profile, it should show v22 and 6 rules […]. To get it sooner, trigger an MDM check-in for this Mac in Zentral.

So I poked my Mac. Declarative Device Management picked up the new legacy profile declaration, and installed the v22 profile.

Admin

what about now?

Claude Code · tool call santactl status
>>> Watch Items
  Enabled                                  | Yes
  Data Source                              | Embedded in configuration profile
  Policy Version                           | v22
  Rule Count                               | 6
  Last Policy Update                       | 2026/11/05 07:13:27 +0100

Six rules. santactl doctor needs root, and the agent does not have it, so it asked me to run it:

Admin sudo santactl doctor, in my terminal
=> Validating configuration...
[+] No configuration errors detected

=> Validating sync...
[+] Sync is enabled
[+] Sync is using HTTPS
    Sending preflight request to https://zentral.example.com/public/santa/sync/preflight/santactl-doctor-test
[+] Preflight request succeeded

From the first message to this output: 8 minutes and 20 seconds.

What made this work for an agent

Nothing in this session is specific to Santa. It worked because of how the configuration is managed:

  • The configuration is text, in one place. The agent could read the profiles, the Terraform resources and the Git history, and learn our conventions from them: versioned profiles, new UUIDs, old versions kept. A console with forms and buttons gives an agent much less to read.
  • Terraform is the interface to production. The agent did not need access to Zentral. It wrote a change, and the plan said exactly what this change would do, including the drift no one had asked about.
  • CI/CD does the deployment. Plan on the pull request, apply on main, with OIDC. The agent only had to read the logs.
  • The review stays with people. The agent proposed, two people reviewed, one approved. Branch protection applies to the agent like to anyone else, as long as its token cannot bypass it.
  • The result is visible on the device. santactl status, the policy Version, and santactl doctor let the agent check the outcome itself, instead of trusting that “apply complete” means “fixed”.

This is what an agentic workflow for admin work looks like to us: the agent does the investigation, the change and the verification; people keep the decisions and the approvals.

How to roll out a new profile version to a few Macs first

In this session, the new profile went to the whole fleet at once: default_shard = 100 in the plan above. For a rule rename on our own Macs, that was fine. On a customer fleet, we would not do it this way.

The same zentral_mdm_profile resource can roll out a new version progressively. A device gets the newest version of the artifact that it is in scope for, so you can scope v22 to the test devices first, and keep v21 for all the others:

resource "zentral_mdm_profile" "santa-default-configuration-22" {
  artifact_id   = zentral_mdm_artifact.santa-default-configuration.id
  source        = base64encode(templatefile(/* … */))
  macos         = true
  version       = 22
  default_shard = 0
  tag_shards = [
    {
      tag_id = zentral_tag.santa-testers.id
      shard  = 100
    }
  ]
}

Then you raise default_shard in the following pull requests, for example 10, 50, 100. The shard of a device is a hash of the version and its serial number, so it does not change between steps: the Macs in the first 10% are still in at 50%, and each step only adds devices. Each step is a reviewed change with a plan, and the agent can prepare each one and check the devices in between.