Claude Code Fixes Our Failed Deployments, and a Person Merges the Fix

AI · Oct 26, 2026 · 10 min read

Fundaia is the deployment platform we built for ourselves: it takes a GitHub repository to its own domain, with a database. When a deployment fails, there is a button in its menu that reads Fix with Claude. Pressing it hands the failure to Claude Code, which reads the log and the repository, writes a test that fails because of the bug, fixes it and leaves the change as a pull request. This post is about that button and, above all, about everything around it: what the agent can touch, who approves the change and what happens when it cannot fix anything.

What Fundaia does, briefly

A project in Fundaia holds services. A service usually comes from a GitHub repository: the platform builds an image, starts it in a rootless podman container and publishes it at its own address. Variables can point at each other, so an application reads its database address from the Postgres next to it without anyone copying it by hand.

Every deployment goes through the same six stages: queue, code checkout, build, start, health check and going live. The new container only takes traffic once it passes the health check. If a deployment fails on the way, the previous one keeps serving, so a red deployment is a problem to solve, not an outage. That is what makes it reasonable to let an agent look at it calmly.

First, a diagnosis without AI

Before any model, there is a diagnosis made of plain rules. It reads the state of the service and answers with a verdict (healthy, needs attention, failing, asleep or idle), its findings ordered by severity and, for each one, the next command to run. It calls no model, so it costs nothing and gives the same answer twice.

  • The latest deployment failed, at which stage and with what message, and whether an older one is still serving.
  • The container stopped by itself, or runs but does not answer its health check.
  • It has restarted three or more times without anyone asking.
  • A variable points at another one that does not exist, so the container receives the literal text.
  • Errors that keep repeating in its logs, grouped by shape.

Many failures end here: a variable that points at nothing does not need an agent. Claude Code comes in when the cause is in the code or in the build recipe, and only if a person asks for it.

What happens when you press Fix with Claude

The failed deployment becomes an incident: its failure message is the sample (up to 2,000 characters) and the last 60 lines of its log are the context. From there the run follows four steps, each one visible live in the panel: gather the context, diagnose, reproduce and fix, plus a fifth, opening the pull request, that belongs to the platform and not to the agent.

From a failed deployment to a pull request: the platform sends the log to a Claude Code container that only edits a throwaway clone, checks the diff, opens the pull request itself and leaves the merge to a person; if the agent cannot fix it, it stops and the person can open an issue.

Who does what when a deployment fails. The agent only edits files; the platform pushes; a person merges.
  1. Diagnose. The agent reads the repository and answers only with a JSON: the cause, the files involved, its confidence (high, medium or low) and whether it believes a test could catch the bug. It changes nothing.
  2. Reproduce. It writes a test that fails because of that bug, in the framework the repository already uses, and runs it to check it fails for the right reason, not because it does not compile.
  3. Fix. It makes the smallest change that makes the test pass, and keeps the test. The prompt asks it not to reformat, rename or tidy anything along the way: whoever reviews it is reviewing it precisely because a machine wrote it.

Each step is a separate call to the Claude Code component, which runs claude in non-interactive mode with a ten-minute limit per prompt. The platform reads the answer of each step and decides whether to go on. These are the checks, taken from the code:

const diagnosis = parseAnswer<Diagnosis>(answer)

if (diagnosis === null) throw new Error('El agente no devolvió un diagnóstico legible')
if (diagnosis.confidence === 'low') {
  throw new RunStopped(
    'rejected',
    'El agente no encontró la causa con los logs disponibles, y no ha tocado nada',
  )
}
// ...
if (!reproduction.reproduced) {
  throw new RunStopped(
    'notReproducible',
    reproduction.note === ''
      ? 'No se pudo reproducir el fallo, así que no se ha cambiado nada'
      : reproduction.note,
  )
}
// ...
if (!fix.fixed) throw new RunStopped('rejected', 'El agente no consiguió arreglarlo')
if (diff.trim() === '') throw new Error('El agente dijo haberlo arreglado sin cambiar nada')

What the agent can touch

Inside its container, Claude Code runs with permission prompts turned off, because nobody is there to answer them. That only makes sense if the container itself is the limit, so the limit is built in layers:

  • Each prompt works on a fresh, shallow clone of one branch (depth 1), with git hooks disabled and the remote removed after cloning. When the prompt ends, the clone is deleted.
  • The clone uses a GitHub token that can only read the repository's contents and expires within an hour. The agent never holds a credential that can push.
  • The container drops every Linux capability except the ones it needs to stop being root, cannot gain new privileges and runs Claude Code as an ordinary user.
  • What the platform takes back from the container is the agent's answer and a diff of what it changed. Nothing more.
const clone = Bun.spawn([
  'git',
  '-c', 'credential.helper=',
  '-c', 'core.hooksPath=/dev/null',
  'clone', '--depth', '1', '--single-branch', '--branch', branch,
  // ...
])
// ...
await git(target, ['remote', 'remove', 'origin'])

There is no network credential here that can push: the platform takes your changes as a patch, validates them, and opens the pull request itself. Editing files is the whole of what you can do, and it is enough.

The text every one of the three prompts starts with

Before anything reaches GitHub, the platform checks the diff with its own validator, without a model. Some things stop the run. Others the validator classes as needing a person: they do not stop it and they reach the pull request, which nobody merges automatically. The prompt already asks the agent to leave CI workflows, git hooks and install scripts alone unless the failure is literally in one of them:

What the diff validator checks
The changeWhat the platform does
A path that points outside the repositoryStops the run
A file that looks like a credential (.env, private keys, .pem, .key…)Stops the run
An added line shaped like a secret (Anthropic, GitHub or AWS keys, private keys)Stops the run
CI workflows, git hooks, .npmrc or other files that decide where dependencies come fromLets it through to the pull request
An install script in package.json (preinstall, postinstall…)Lets it through to the pull request
Binary files, more than 40 files or more than 2,000 added linesLets it through to the pull request
const credentialLike = [
  /(^|\/)\.env(\..+)?$/,
  /(^|\/)id_(rsa|dsa|ecdsa|ed25519)$/,
  /\.(pem|key|p12|pfx|keystore)$/,
  // ...
]

const secretPatterns = [
  /\bsk-ant-[A-Za-z0-9_-]{16,}/,
  /\bgh[pousr]_[A-Za-z0-9]{16,}/,
  /\bAKIA[0-9A-Z]{16}\b/,
  // ...
]

Only after that does the platform apply the patch on its side, commit it and push the branch with a second token, one that can write contents and pull requests and never leaves the platform's process. It travels in the environment, not on the command line, where any user on the machine could read it.

Who approves it

A person, always. The platform opens the pull request on a branch of its own, labels it autofix and writes the body for whoever has to review it: what was failing, why, the command and the output of the test that failed before the change, what changed and what could still go wrong. It opens with a plain sentence: nobody has reviewed this yet, that is your job.

Fundaia never merges these pull requests. When someone on the team merges one, the branch moves and the platform deploys it like any other push. The platform also looks at who is behind that push: the commit's author is its own bot, Fundaia Autofix, but a merge by a member of the workspace vouches for it. A push that nobody in the workspace vouches for waits until a member approves it.

When it cannot fix it

Most of the design is about this case. Every way of stopping has its own name, and in all of them the incident goes back to open, so nothing is left half-claimed:

How a run can end
What happenedHow it ends
The agent opened the pull requestDone, and the incident waits for a person to review the fix
The log is not enough to find the cause (low confidence)Discarded, without touching anything
It could not write a test that failsNot reproducible, with the agent's reason in one sentence
It tried and could not fix itDiscarded
It said it had fixed it but the diff is emptyFailed
The diff touches something it cannot touchDiscarded, naming the file and the reason
It reached the cost ceilingDiscarded

From the same menu, a failed deployment can become a GitHub issue with its evidence: the commit, the failure message and the last lines of the log. The token for that can only write issues. Asking twice links the same issue instead of opening another, so a team that prefers to fix things by hand loses nothing by trying the agent first.

Limits on money and frequency

An agent that loops on a stubborn bug can spend a lot. These are the defaults of each service:

the automatic mode, until someone turns it on
Off
runs per service per day, also when a person asks
3
cost ceiling per incident, checked after every prompt
2 €
the limit of each prompt inside the container
10 min
  • The agent is paid for with prepaid credit. When it runs out, the agent stops instead of building a bill nobody has seen.
  • The cost is known when each prompt answers, so a run can go past the ceiling by at most one prompt, and then it stops.
  • Fixing a failed deployment always has to be asked for, from the button, the command line or an MCP tool; the platform never starts it on its own. The automatic mode only covers errors that repeat while the service runs, from five occurrences by default, with one run per service every five minutes at most.
  • The default model is Sonnet. Each service can choose another one, and the run is charged to the same credit as any prompt.

What we take from it

We have had the autofix on runtime errors since August 2026 and on failed deployments since September. The part that uses AI is three prompts. The rest is code that does not trust it: a deterministic diagnosis first, a container that can only edit files, a validator that reads the diff, a ceiling on money and a person who decides.

That is the same split we use when we build AI agents for a client's processes: the model proposes, plain code checks what it proposed, and the step that cannot be undone is a person's. It is also how we think about software maintenance: the agent saves the first hour of reading logs, not the review. If you are choosing the model for that kind of work, we compared the current ones in Opus 5.5, GPT-6 Astra or GPT-6.1 Sol.

Questions

Can the AI deploy something to production by itself?

No. It edits files in a throwaway clone and hands back a diff. The platform validates it and opens a pull request, which it never merges. A change only reaches production when a person on the team merges it and the usual deployment picks it up.

Does the agent see the service's secrets?

It receives the repository, the failure message and the last lines of the log, not the service's variables. If the application prints a secret in its own logs, that line travels like any other, which is a reason not to log secrets. Going the other way, a change that adds a credential file or a line shaped like a key stops the run before it reaches GitHub.

What if the agent gets the cause wrong?

That is why it has to reproduce first. If its diagnosis has low confidence, or it cannot write a test that fails because of the bug, the run stops without changing anything. If it gets through, the pull request carries the test and its output, so the reviewer can check the claim instead of trusting it.

How much does each fix cost?

It depends on the model and on how much it has to read, so we do not give an average. What is fixed is the ceiling: each incident has a cost limit (2 € by default), checked after every prompt, and the agent runs on prepaid credit that stops it when it runs out.

Sources

  1. Fundaia: Arreglos con Claude Code
  2. Fundaia: Claude Code
  3. Claude Code: Run Claude Code programmatically
  4. Claude Code: Development containers
  5. Claude Code: Security
  6. GitHub: Generating an installation access token for a GitHub App
  7. Podman: podman run

Keep reading

Blog