July 3, 2026
10 min read
ai / devops / workflow
From a Bash Loop to Guarded Orchestration for AI Agents
I built a guarded task runner around my AI workflow so agent work happens in disposable containers, returns as reviewable Git patches, and can grow beyond a single Bash script.

One problem kept bothering me as I pushed deeper into AI-assisted development.
I was getting better results from my workflow, but I was still relying on a runtime model that gave the agent too much authority over my machine.
The tasks were getting better. The process was getting better. The repository documentation was getting better.
But the execution layer was still too thin.
That is why I built a new orchestration tool.
At a high level, it is a guarded task runner that executes one repository task at a time inside a hardened disposable container, exports the result as Git patches, and imports those patches back into a reviewable branch before merging them into the main checkout.
The point was not to make agent work look clever. The point was to make it safer, more structured, and more compatible with the AI workflow I have been building.
The original problem was not prompting
Before this orchestration layer, the fast path was still direct agent execution against a live checkout.
In practice, that meant a command path built around codex exec --dangerously-bypass-approvals-and-sandbox running close to the real repository state. That is useful when I want speed, but it is a bad default when the whole premise of the workflow is to reduce ambiguity and tighten control.
The risk was simple:
- a mistaken edit could land directly in the principal checkout
- a destructive command could hit the real machine
- the runtime had very weak boundaries compared to the care I was putting into planning and task design
That mismatch started to matter more as my workflow got more serious.
I already had rich task plans, project memory files, PRDs, and a clearer TDD-oriented loop. The weak point was no longer the task definition. It was the shell around the task.
The Bash version proved the idea, but it also showed the limit
I do not think the original Bash approach was a mistake.
It was the fastest way to prove that my AI workflow could work at all.
The tasks themselves were already useful because they carried real context: status markers, affected files, acceptance criteria, tests, and execution order. That meant I was no longer asking the agent a vague question like "build this feature somehow." I was asking it to continue a repository-defined process.
That was already a big improvement.
But once I saw that the workflow was actually helping, the orchestration layer started to feel underbuilt.
I wanted better safety. I wanted better visibility. I wanted a better operator experience. And I wanted something more robust than one long Bash entry point controlling everything.
So I treated orchestration itself as a project instead of as an accessory script.
What I built
The current implementation lives in my skills/orchestration subproject.
It is split into a few clear parts:
run-task-loop-guarded.shas the host-side orchestratorguarded-container/Dockerfileas the default Codex-compatible runtime imageguarded-container/entrypoint.shas the container helper that runs the agent, validates artifacts, and exports patch historytests/run-task-loop-guarded.bashas a black-box test suite using stubbedtreehouseanddockercommandsPROJECT.md,CONTEXT.md,docs/PRD.md,docs/TASKS.md, and an ADR describing the contract
That last point matters to me.
I did not want this to become undocumented shell folklore. If I am going to rely on orchestration for AI-assisted work, I want the orchestration layer itself to have the same discipline as the projects it runs.
I also built it incrementally instead of trying to get everything right in one pass.
The task-run history shows that progression clearly:
- first I tightened the black-box test seam around fake
treehouseanddockertooling - then I enforced the host-side artifact contract, especially around exported patches and commit metadata
- then I tightened the container-side export rules
- then I documented the operator contract and added
--help - after that, I extended the runner with bounded multi-task support through
--max-tasks, while still reacquiring a fresh leased worktree for each task
That sequence is important because this project is about reducing hidden behavior. Building it through small tested stages was part of the design, not just the implementation style.
How the guarded loop works
The guarded runner starts from a few assumptions:
- the principal checkout must be clean
- runnable work must exist in
docs/TASKS.mdordocs/tasks/*.md treehouseand Docker must be available
From there, one iteration looks like this:
- The host runner finds a runnable task plan entry with
Status: readyorStatus: planned. - It leases an isolated worktree through
treehouse. - It clones that leased worktree into a disposable snapshot repository.
- It overlays the leased working tree onto the snapshot so local generated files are available without exposing a writable mount of the real worktree.
- It writes a prompt file that tells the agent to use repository files only, select exactly one runnable task, update the task plan, write task metadata artifacts, and stop after one task.
- It starts a hardened container against the snapshot repository and a narrow artifacts mount.
- Inside the container, the helper runs the agent, optionally commits uncommitted work if the agent left changes behind, and exports the result as a
git format-patchseries. - Back on the host, the runner imports the patch series into a named branch using the pattern
agent/<task-id>-<run-id>. - If the import succeeds, the host fast-forwards that branch back into the principal checkout.
- The lease is returned, and the next task starts from a fresh leased worktree if the configured task limit has not been reached.
The key boundary is this one:
The agent never gets a writable mount of the principal checkout or even the leased worktree itself. It gets a writable disposable snapshot. The only intended persistence channel back to the host is explicit Git history plus a few task metadata artifacts.
That artifact contract is intentionally narrow:
selected-task-id.txtlast-commit-message.txtpatches/*.patch
If those artifacts are missing when they are required, the guarded flow fails fast.
The hardening details matter
The Docker boundary is not just decorative.
The current runner builds the container with a fairly strict default posture:
docker run --rm --init \
--network none \
--cap-drop=ALL \
--security-opt no-new-privileges \
--pids-limit 512 \
--memory 4g \
--cpus 4
The image itself runs as a non-root node user and uses a dedicated entrypoint that checks for prompt and artifact requirements before and after the agent executes.
This is not a perfect sandbox, and I do not want to overstate it.
But it is materially better than giving an agent direct write access to the live repository and full freedom on the host by default.
One detail I especially like is that the default network mode is none.
That is the right safety default, even though it creates friction. In fact, one of the implementation discoveries recorded in the project is that a real hosted Codex run needs an explicit override such as GUARD_NETWORK_MODE=bridge because the safe default blocks remote API access.
I consider that a useful trade-off, not a bug. If network access changes the risk profile, I want it to be visible and intentional.
Why I think this works in my environment
I do not think this kind of orchestration works well in every environment.
I think it works in mine because the rest of the workflow is already repository-shaped.
The runner is not trying to invent work from scratch. It is operating on files that already define the work:
PROJECT.mdexplains the projectCONTEXT.mdcarries evolving memory and constraintsdocs/PRD.mddefines the accepted behaviordocs/TASKS.mddefines runnable units with status markers
That makes the runtime simpler and more reliable.
The prompt created by the runner does not depend on hidden chat history. It tells the agent to read those files, pick one runnable task, execute only that task, record the selected task id, and stop.
That is the part that makes orchestration possible.
The runner also leans on Git very heavily as the persistence model. I like that because Git already gives me review, diff, branch naming, history, and a clear definition of what crosses the boundary and what does not.
Another reason I trust this more than an ad hoc script is the test suite.
The black-box tests do not inspect internal shell functions. They stub treehouse and Docker, invoke the public CLI, and assert behavior such as:
- hardened Docker flags are present
- the container does not receive the leased worktree as a writable mount
- missing patch exports fail the run
- missing commit metadata fails the run
- dirty principal checkouts are rejected
--max-tasks 2reacquires a fresh lease for the second task instead of reusing state
That test seam is important because orchestration bugs are rarely obvious when you only read the script. They show up in the edges between Git state, task state, and runtime state.
The trade-offs are real
I do not want to present this as free safety.
The guarded path is slower than the direct unsafe path. It is more complex. It depends on more moving parts. It introduces more rules around artifacts and task metadata.
It also gives up convenience on purpose.
Untracked caches do not automatically survive. Container-local state disappears after the run. The network default is intentionally restrictive. The host runner has to manage leases, imports, merges, and failure cleanup carefully.
That is a lot more machinery than "run the agent in the current directory."
But that is exactly the point.
When an agent is executing code changes with dangerous bypass flags, simplicity at the command line can hide complexity in the risk profile. I would rather make the runtime machinery explicit than pretend the risk is not there.
What still needs to improve
This project is useful now, but it is still early.
The tasks are strong. The workflow is strong. The orchestration surface is still rougher than I want.
The biggest gaps I see right now are:
- better UI for understanding a run while it is happening and after it finishes
- better visibility into selected tasks, exported artifacts, validation output, imported branches, and failure reasons
- a more robust implementation model than one large host-side Bash script coordinating most of the flow
- better review ergonomics around branch promotion, discard, inspection, and rollback
- a clearer security and credential story for local versus hosted agent execution modes
- richer run history and observability instead of mostly temporary directories and log files
In other words, I think the orchestration logic is becoming good enough, but the product experience around it is not there yet.
That matters to me because I do not want this to stay as a private engineering trick. I want it to become a real tool I can trust, inspect, and improve deliberately.
Why I built this at all
If I compress the whole project into one sentence, it is this:
I built this because my AI workflow was getting better faster than my execution boundary was.
The danger of running agents with broad bypass flags made the problem obvious, but security was only half of the reason.
The other half was workflow.
I already had a system that defined how work should move from context to PRD to task plan to implementation. I needed an execution layer that respected that model instead of bypassing it.
That is what this orchestration tool is trying to become.
Right now, it is a guarded loop with clear contracts, good tests, and honest limitations. Over time, I want it to become the stronger runtime layer beneath the AI development process I am building.
That feels like the real next step.