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 azentral_mdm_profileresource that points to a.mobileconfigfile. 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 planon every pull request and posts it as a comment. After the merge, CI runsterraform apply. It authenticates to Zentral with an OIDC token, so there is no long-lived secret in the pipeline. mainis 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,plutiland Terraform, in a container; - push a branch and open a pull request with
gh, and read the CI logs; - run
santactl statuson 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
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:
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:
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:
>>> 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
Santa accepts only letters, digits and
. _ : -in a rule name, so the space inSSH foldermakes that rule invalid. […]
- New profile v22. I copy v21 to
mobileconfigs/santa.default-configuration.v22.mobileconfig, then in the copy:
- rename
SSH foldertoSSHFolder, in the same style as the other rules (GoogleChromeCookies,Keychains, …);- change
FileAccessPolicy→Versionfromv21tov22, sosantactl statusshows which policy is loaded;- give the payload and the profile new
PayloadUUIDs, as was done from v20 to v21.- Terraform. I add
zentral_mdm_profile.santa-default-configuration-22after the v21 resource […]. The default and Entra blueprints already use this artifact, so they need no change.- Checks before a PR:
plutil -linton the new file; check every rule name against Santa’s regex again;terraform fmt/validatein thehashicorp/terraformcontainer.- Check on a Mac after the apply:
santactl statusshould showPolicy Version v22andRule 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
Yes, make 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:
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
verify plan on ci
The agent read the log of the “Terraform Plan” job:
# 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.
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:19hasXYZ0123456. So someone changed the serial on the server, outside Terraform, and the apply after the merge would set it back toXYZ0123456.
XYZ0123456ZZZlooks 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.
Yes, set it back
The review rule applies to the agent too
I couldn’t merge the PR: branch protection on
mainneeds 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
squashed and merged
The agent confirmed the merge, found the “Terraform Plan/Apply” run on main, and waited for it:
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:
This Mac hasn’t installed v22 yet.
santactl statusstill showsPolicy Version v21withRule Count 5, so the fix isn’t confirmed on a device. Once the MDM installs the profile, it should showv22and6rules […]. 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.
what about now?
>>> 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:
=> 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 policyVersion, andsantactl doctorlet 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.