As I started using Claude Code across my projects, I quickly ran into friction – and frustration. The promise of hands-off development turned into a security problem – in trying to corral my AI agents and not let them go wild on my machine and access sensitive areas – while still giving them enough room to maneuver to get the tasks completed. And it also turned into a micro-managing problem - in sorting out why a given worker failed, and how to implement mechanisms to avoid that issue from happening the next time.
The lesson I kept relearning is that this is not a configuration problem – it’s an observability problem. The more introspective I got into the runtime workings of agents, the more effectively I could direct them away from undesired behavior and more aimed toward resolving their assigned goals.
When agents go wild
During my Claude Code work, there were multiple instances of worker sessions going into unexpected and undesired actions across my filesystem, such as running broad searches like find ~/Library, trying to cat all files in my HOME dir, or attempting to change files in other directories.
The immediate impact on my dev efforts was the constant permission prompts (macOS TCC – Transparency, Consent & Control – and Claude Code prompts), which stopped my processes until I approved the action. My initial frustration led me to consider enabling --dangerously-skip-permissions so agents have free rein to do as they please and I’d just “surrender”. That didn’t sit well with me - as I want to leverage the power of AI without having to give up control over the process in the pursuit of the outcomes I wanted.
Whenever these situations arose I ended up trying to answer the same questions: What led the worker down that execution path? How could the worker instructions be refined and tightened? Which control levers are available to apply to this scenario?
This check/adjust process was tedious but manageable at the onset of my work with AI, but quickly went downhill as more workers were running concurrently across projects.
Bringing in the Gatekeeper
This quest for a smoother Dev<->Agent relationship led me to integrate a single Policy Enforcement Point in a Dispatcher that would serve double duty: Spawn workers with a standard set of rules and guidelines for execution, and introspect into their process and output end to end until completion.
That one change simplified my workflow, but the one key element it gave me was observability at scale. When a worker attempts to act, the dispatcher has the wider context around the request: what the worker was doing at that time, what led it down that route, what command triggered the event, and the ability to introspect after-the-fact on ways to mitigate or eliminate an undesired action.
Here’s a recent example:
A pipeline I run publishes a customer-facing artifact: an audio version of an online publication with an active and engaged audience. Each edition has to pass gates guarded by my approval signature. I review the edition, sign, and my signature is bound to a digest/fingerprint of exactly what I approved. The Publishing stage re-generates that fingerprint and refuses to publish anything that doesn’t match. That check is the only thing standing between the review and the wrong edition reaching listeners.
On the 7th edition of the publication, I signed the publication as usual but the publishing step refused to complete the task. This happened right on the due date of the edition needing to go live.
The dispatcher report identified the discrepancy between the approval and verification fingerprints as the reason for the refusal. During the investigation, I had the dispatcher run a series of experiments, each running the signing and publishing process to understand what had regressed. Through that process, the dispatcher identified a pattern across the experiments: while the edition artifacts remained constant, the build timestamp changed and was included in the fingerprint calculation. Turns out this was a regression from an earlier code edit.
The test suite was all green, and there was a test in place specifically to prevent this type of regression. Upon further inspection, the test did the expected: run the process twice and compare the digests. However, its input fixtures did not include the timestamp in the same way as the runtime did, so the harness always passed.
The fix for this was two-fold: exclude the build timestamp from the fingerprint generation, as it did not make sense to include it. Then after that, solidify the runtime by having the approval stage run the process twice and refuse to hand me anything to sign in the first place if the two runs disagreed.
All this occurred while I was away from my desk and on my mobile. The discovery and resolution of this issue was possible due to a rule in the dispatcher’s configuration that states nothing reports success without the evidence for it. Instead of a process stating “the publish failed”, the actual receipts (comparison checks, exit codes, log entries, etc) that back the assertion are also exposed from the workers up to the dispatcher, thus bringing detailed observability across the fleet. Root cause and resolution then become clearer to execute, whether by the dispatcher or me, to keep the fleet unblocked in their tasks.
Agentic workers aren’t deterministic code with a fixed execution path. They act with broad authority and choose their own route through a task. Runtime introspection is the only reliable control surface for that, which is what makes it first-class rather than just a good debugging habit.
In a future article I will go more in-depth on how to configure the dispatcher<->worker flow.
Setting the Baseline
Constraint and observability are two halves of the same problem. You constrain with a static deny-list and a set of rules decided up front. That’s the cheap, static half, but you only learn what belongs on that list, and why a worker did what it did, by watching it run. The rest of this section is that static half – the deny-list and rules I set up front.
On the worker permissions side, my first attempt at control was the obvious one: write a per-command allow rule for each command a worker legitimately needs. That failed quickly, and for a good reason. There’s too much variability in how bash commands are structured in real agentic work. Flags land in different positions, all valid for the command, piped and quoted into one-liners. Pre-whitelisting every valid permutation of those patterns became a work in itself. Again I was back where I didn’t want to be - glued to my desk approving, adjusting and testing, with my real-work productivity going down the drain.
So it was time to evaluate and make a trade-off - focus on getting the business value of AI on my projects and remove myself as the manual approver of permission requests. During those early sessions, I noticed that most of the commands the workers ran were valid permutations of rules I already had in place. It did not make sense to keep spending time chasing the perfect allow-list.
So I switched to identifying the real outliers instead: the actual commands (like security, sudo, and others) that are more likely to brick the system and have a straight ban on those. Beyond that, I rely on my OS’s own mechanisms (TCC, Keychain access), and then on dispatcher instructions to workers. Here are some of the baseline deny rules I run today:
“deny”: [
“Bash(security:*)”,
“Bash(sudo:*)”,
“Bash(osascript:*)”,
“Bash(open -a:*)”,
“Bash(rm -rf /:*)”,
“Bash(rm -rf ~:*)”,
“Bash(rm -rf ~/.claude:*)”,
“AskUserQuestion”
]The overall idea isn’t the specific list or instructions above, it’s the shape of the decision: ban the few commands that can do real damage, and delegate the long tail to your OS security layers and to the instruction set given to your agentic work fleet.
Rules of Engagement
Beyond the protection of straight-banning worker actions and the OS-native protections, I have the dispatcher add a set of Rules of Engagement as part of the instruction set for workers. These instructions steer workers away from actions not pertinent to their work, with the side effect of workers saving round-trips and tokens in exploring approaches that were never going to run in the first place.
Here is a sample of the dispatcher instructions to workers as they’re spawned:
- No TCC-triggering commands: no osascript, no open -a, no
security find-generic-password / find-internet-password /
unlock-keychain, no defaults write on protected domains.
- No filesystem searches. Never run find /, find ~,
find ~/Library, ls -R ~, home-scope grep -r, du -h ~, or
disk-scope mdfind. Scope every search to this repo's path.
Every path you need is given below as an absolute path -
if something doesn't resolve, stop and ask rather than
searching for it.
- Never print an unmasked credential value. No env, no
printenv, no ps eww, no cat of shell profiles or .env
files.
- No Task or sub-agent tools - do all work with direct tool
calls. No AskUserQuestion. No Skill or artifact tools.One useful element, and easier for workers to follow, is to ban the mechanism - not the intent.
Providing safe alternatives to unsafe mechanisms goes a long way towards compliance and provides the workers a path to achieve my goals.
Launch app on iOS sim
banned osascript … tell application "Simulator"
sanctioned xcrun simctl launch <UDID> <bundle-id>
Boot iOS sim
banned open -a Simulator
sanctioned xcrun simctl boot <UDID>
Screenshot iOS / Android
banned —
sanctioned xcrun simctl io <UDID> screenshot
adb exec-out screencap -p
Start Metro
banned expo start (TTY / foreground / --ios)
sanctioned nohup npx expo start --port <n> > /tmp/<proj>-metro.log 2>&1 &
Android emulator
banned emulator -avd <name>
sanctioned emulator -avd <name> -no-window -gpu swiftshader_indirect
Find a file
banned find ~ / find ~/Library / ls -R ~/
sanctioned find <project-path> — scoped, alwaysAn important element to keep in mind: Even with explicit instructions, workers may still sometimes deviate and attempt an unsanctioned path due to their stochastic nature and depending on the underlying LLM model used.
Not a Bed of Roses
Introducing a Dispatcher in the midst of my workflow has brought with it a whole other set of issues. From its initial configuration, startup boot/session-state handoff, and quirks, it does take a toll on my direct, hands-on availability for my core workload.
With the Dispatcher orchestrating, I need to be mindful of catching any relevant behavior pattern or special case, and have the dispatcher update its own DISPATCHER.md configuration to adjust or prevent undesired dispatcher behavior in future iterations. I have had to tell my Dispatcher when certain workers are stuck asking for a permission or when they’ve veered off course. This adds another maintenance task on me to manage on a daily basis.
Sometimes the dispatcher would fail in reporting the status of a worker. Other times, worse, it would misrepresent the status of a feature or report. This has led me to make decisions based on faulty data which then needed to be corrected by future workers at the end of the day.
An observer you trust that is wrong (often enough) is worse than no observer at all. That’s the basis for the standard that nothing reports success without the evidence for it. This is the harness on the checker, and helps bring order to the chaos of managing a worker fleet.
Neverending Story
None of these checks and balances were designed up front. They were derived from inspecting, checking, and adjusting as I went along. The temptation was, and still is, always there to “just approve” a permissions request or ignore an obvious hurdle that workers run into time after time. I’ve realized, however, that taking the time to solidify the process pays off in less of my hands-on time and increased confidence in the output. This is the feedback/adjustment loop that I continue to run every day.
The dispatcher’s introspection into my agentic workflow has been key to shine a light on where the unwanted behavior comes from, and then have the dispatcher itself add the constraints, as we go along, that further refines the process - one gap at a time.
You can’t constrain as effectively as when you can see. Start by watching.
