Deployed at Home: my path toward Forward Deployed Engineer. Part 3 of 4, from field lessons to tools: the guardrails that let AI agents build without me reading every line.

In Part 2 I described rewriting my guesthouse back office in Go. DeepSeek V4.1 Flash wrote most of the code, running in DeepSeek Harness. It is fast and inexpensive, and like every coding model it will sometimes tell you a task is done when it isn’t.

I didn’t want to solve that by reading every diff myself. I wanted the environment to refuse unverified work.

The environment, and what I can share about it

The environment is a set of guardrails, skills and sub-agents around the coding agent, which I set up for this rewrite with advice from Kiril Iliev of Orpheus Systems. It started as a setup for Claude Code; I reworked it to run inside DeepSeek Harness and to support Go, along with PostgreSQL, templ and the rules of this repository. This article describes the ideas and leaves out the internals.

None of the ideas are exotic. They use the same building blocks as any serious agent setup:

  • Skills: written workflows an agent loads when it needs them, such as “write a plan”, “run a wave of tasks”, “research a question” or “review before a pull request”.
  • Sub-agents with roles: an implementer, a test author, a bug fixer, reviewers, and an adversarial reviewer whose only job is to break the change.
  • Guardrails: rules in code that refuse an action outright, with the reason in the refusal.
  • Hooks and checks: things that run automatically after an edit and before an agent is allowed to call its work done.
  • Tools: the harness’s own tool calls and MCP-style integrations, each with the same guardrails in front of them.
  • A human in the loop: risky actions stop and ask me.

The rule written at the top of the project’s agent guide is “guard rails are code, not requests”. A prompt asking an agent not to touch a file can be ignored. A tool call that comes back denied, with the rule and the reason, can’t, and in practice the agent reads the reason and adapts.

Four layers between an agent and the repository

outside its scope, secrets,

generated code

ok

destructive or hard to undo

ok

red or timed out

green

limit reached

Agent wants to act

1. Guardrail

Refused, with the reason

2. Risky?

Ask the human

Action runs

3. Check right after the change

does it still build?

Agent says it's done

4. Gate

tests, definition of done, report

Sent back to work,

a limited number of times

Done

Stopped and flagged

for a human

  1. Refuse. An agent can only write inside this project’s folder, which is how the old .NET app and my separate landing page stay safe in the same repository. It can’t touch secrets or hand-edit generated code. Agents share one checkout, so only one session is allowed to commit.
  2. Ask. Some actions are sometimes right and always risky: staging everything from the repository root, changing a database migration that has already shipped, dropping data volumes. Those stop and wait for me.
  3. Check immediately. After a code change the project is built at once, so a compile error shows up in the same step instead of three tasks later. A plan with a vague task in it (“TBD”, “handle edge cases”) is rejected when it’s saved.
  4. Gate before done. When an agent says it’s finished, the tests for what it changed run first. If they’re red, it goes back to work. A check that times out counts as not verified, never as a pass.

The gate has a limit on how many times it sends an agent back. A gate that can loop forever is a bug of its own, so after a few attempts it stops and flags the task for me.

Different roles, one model

Every role ran on the same fast model. What changed between roles was how hard it was allowed to think: the most effort went to finding root causes and to adversarial and security review, where a false “pass” has nothing after it to catch it. Implementation got less, because a plan, a test and a review all sit downstream of it. One model also meant one set of credentials and one set of failure modes to learn.

Built to distrust the code

Most of the design follows from one assumption: whatever an agent produced is wrong until something that can fail has passed.

Plan task

with a runnable definition of done

Test author

can't see the implementation

Failing test,

fenced off from the build

Implementer

removes the fence, makes it pass

Definition of done

fails if no test ran

Task review

Wave complete

Adversarial and security review

of the exact diff

Full test suite

unit, PostgreSQL, browser

One commit per wave

The test author is blind. It writes tests from the plan and can’t read the implementation, apart from public interfaces. A test written by someone who has seen the code tends to test the code. A blind one has to test the plan.

Tests carry their task id in the name. TestReservationOverlap_T7 belongs to task T7, and TestDepositRounding_B2 to bug B2, so the definition of done can run exactly one task’s tests.

Failing tests are fenced. A test for code that doesn’t exist yet won’t compile, and a broken build would block every other task. So new tests start behind a Go build tag that normal builds skip, and the implementer’s first job is to remove it.

“No tests ran” is a failure. A Go test filter that matches nothing exits with success, which is exactly the kind of green I stopped trusting in Part 1. The project’s test runner fails when nothing with the task’s id ran.

Reviews expire. A pull request is refused until the adversarial and security reviews have passed the current diff. Any later change invalidates them, and they run again.

Where green was still a lie

The guardrails made false greens visible; turning each one into a rule was still my job. Four times during the rewrite a check passed without examining anything:

  • A locale check accepted --locale=C as UTF-8.
  • A test skipped itself because the test setup was already using port 8080, and the skip looked like a pass.
  • One browser journey was committed with its fence still on, so the build compiled it out and it passed with nothing to run.
  • A cleanup task deleted three assertions, and the remaining tests passed.

The rule that came out of it is in the project’s lessons file: before trusting a check, make it fail, against the mutation, the hostile input or the removed assertion, and treat a skip, a “no tests to run” or a fence as a failure.

The best example of adversarial review earning its cost was a privacy bug. The owner’s guest search term leaked into the logs through the request URL. The first fix filtered the log. The adversarial reviewer found the term again in the Referer header, then in an htmx header, then in the redirect the app itself wrote. It took three rounds. The fix that ended it built redirects from the path only, so what the owner typed never enters a header. The log filter stays as a second layer. The lesson I wrote down: treat any URL the app builds from user input as the bug, and the log filter as a backup.

A real start beats a careful read

Another lesson came from the deployment. The first real podman compose up of the Go production stack found three defects that no review had: the seeder refused to run when no owner existed yet, podman-compose 1.6.0 prefixed a volume name so a token file was never found, and Zitadel restart-looped because the admin password failed its own password policy. The same thing had happened with the .NET stack in Part 1. So the rule is now that any task that changes the deployment includes one real start, in the same wave.

If you want to build something like it

  • Put the rules in code that can say no, with the reason in the refusal. Agents adapt well to a clear refusal and poorly to a paragraph of instructions.
  • Give agents one writer for git when they share a checkout.
  • Put a limit on every gate.
  • Treat timeouts, skips and empty test runs as failures, everywhere.
  • Name tests after tasks, and make “nothing ran” an error.
  • Keep the test author away from the implementation.
  • Tie every review to the diff it reviewed.
  • Write every false green down as a rule, with the evidence.

What this has to do with forward deployed work

Forward deployed engineers are expected to bring lessons from the field back into something reusable, so the next deployment starts further ahead. Most of the rules above can be traced to a specific failure: the CPX12 verdict from Part 1, the four false greens, the leaked search term, the three defects found by a real start.

It’s also normal in client work to depend on tools you can’t show. You work inside someone else’s licence or someone else’s platform, and you explain the principles without handing out the internals. That’s how this article is written, and I think it’s a fair test of whether the ideas stand on their own.

There’s also a practical reason this matters to me. On a client’s system I can’t read every line an agent writes, and I can’t afford for “done” to mean “the agent said so”. Guardrails in code, tests named after tasks and reviews tied to a specific diff let me move at agent speed in someone else’s environment and still show evidence for every claim.

Next: Part 4, how Claude Code designs and reviews while DeepSeek builds, and what it was like to have them talk only through two append-only files.