Debugging a Production VPS Incident With Claude Code

For investigation, yes and it is genuinely faster. For remediation, only with guard rails: run the investigation read-only, block irreversible commands with a PreToolUse hook, and make any fix a separate reviewed step. The risk is not that the agent is careless, it is that a confident wrong hypothesis plus shell access is expensive.
Because a response from a CDN or caching proxy tells you what the edge remembers, not what your application currently does. Resolving the hostname to the origin and comparing the two responses takes seconds and immediately tells you whether you are debugging your code or your cache.
Restarting, pruning or redeploying. Those destroy the evidence that explains the failure, and they often appear to fix it, which guarantees the same incident returns later with no diagnostic trail. Capture logs, process state and configuration first, then change one thing deliberately.
Assert the outcome, not the exit code. End the deploy by checking that a build identifier served over HTTP matches the one just built. In the incident that taught me this, the new process had failed to bind because the old one never exited, and the pipeline reported success the whole time.
At least one automated check. A stale-deploy incident becomes a post-deploy build identifier assertion, a caching incident becomes a synthetic request that bypasses the edge, a disk incident becomes an alert threshold. A fix without a check is a memory, and memories expire.

Key Takeaway
An AI agent is excellent at gathering incident evidence in parallel and dangerous at acting on a guess. The safe pattern is read-only investigation with a PreToolUse hook blocking destructive commands, testing the origin directly rather than through a CDN, and requiring a stated hypothesis before any change.
The site was up, the deploy had reported success, and the page in the browser was two versions old. Nothing in the application logs was wrong, because nothing in the application was wrong. That incident took twenty minutes to resolve with an agent and would have taken me an hour alone, but it also showed me exactly where letting an agent loose on a production box goes badly.
This is the working method I settled on: what an agent should do during an incident, what it must be prevented from doing, and the evidence order that keeps both of us honest.
Incident response is mostly evidence gathering under time pressure, which is a good match for something that can run ten commands while you think about the eleventh.
What it is bad at is knowing when it is wrong. A confident wrong hypothesis plus shell access is how a small incident becomes a large one, which is why the guard rails below are not optional.

If a site sits behind a CDN or a caching proxy, the first thing to establish is which layer is answering. A response from the edge tells you what the edge remembers, not what the application currently does, and hours get lost debugging code that is already correct.
# Rule one of debugging a site behind a CDN: stop asking the CDN.
curl -sI https://www.example.com/ | head -20 # what the world sees
curl -sI --resolve www.example.com:443:127.0.0.1 \
https://www.example.com/ | head -20 # what the origin says
# If those disagree, the bug is in the cache layer and no amount of
# reading application code will find it.
# Same discipline for the redirect chain, which hides a surprising
# number of "the site is down" reports.
curl -sIL http://example.com/ | grep -E '^(HTTP|location:)'The same discipline applies to the redirect chain and to hostnames. A rule that sends the bare domain to the www host, or an application that only listens on one of them, produces symptoms that look like an outage and are actually configuration. Following the chain explicitly turns a mystery into one line of output.
Write the origin-versus-edge comparison into your runbook as the literal first step, with the exact commands. During an incident nobody invents good diagnostics; they run whatever they can remember, and what they remember is whatever is written down.
Here is the case that taught me the most, reconstructed from the commands that actually mattered. The application was containerised, the deploy pipeline was green, and the served content was stale.
# The incident: a deploy "succeeded", the site kept serving old content.
# The evidence that mattered, in the order it was gathered:
docker compose ps # container up, restarts: 0
docker compose logs --tail=100 web # no errors, no recent boot line
ss -tlnp | grep :3000 # PID owning the port is the OLD process
curl -s localhost:3000/ | grep -o 'build-[a-z0-9]*' # old build id
# Root cause: the new process failed to bind because the previous one had
# never exited. The deploy script only checked that its own command
# returned zero, so it reported success while nothing had changed.The lesson generalises well beyond this stack. A deploy script that checks only its own exit code is measuring the wrong thing: what matters is whether the running process is the new one. Since then, every deploy of mine ends by asserting that a build identifier served over HTTP matches the one just built — a two-line check that would have turned this incident into a failed pipeline.
Never let an agent restart, prune or redeploy as its first action during an incident. Restarting destroys the evidence that explains the failure, and half the time it also appears to fix the problem, which guarantees the same incident returns next week with no diagnostic trail.
These are the instructions I keep in the project's memory file so they apply to every incident session without being retyped.
That last habit is worth spelling out because it produced a false conclusion for me once: output that appeared to describe a remote repository was in fact describing the local one, and I believed a merge had happened that had not.

Permission prompts protect you when you are reading them. During an incident nobody reads them, which is exactly when you want a rule that cannot be talked out of. A PreToolUse hook that inspects the command and denies a short list of irreversible operations is the cheapest insurance available.
// A PreToolUse hook is the difference between an assistant that can
// investigate production and one that can break it. This one refuses
// the small set of commands nobody should run during an incident.
#!/bin/bash
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command')
if echo "$cmd" | grep -Eq 'rm -rf /|docker system prune|DROP TABLE|truncate '; then
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse",
"permissionDecision":"deny",
"permissionDecisionReason":"destructive command blocked during incident"}}'
exit 0
fi
echo '{}'Pair it with a permission mode chosen deliberately for the session. Investigation runs read-only; the moment a fix is agreed, that is a separate, narrower session with the change reviewed by a human before it runs. Mixing investigation and remediation in one loose session is the setup for the second incident.
Six steps, in this order, every time.
Step six is the one that compounds. Nearly every incident I have had twice was one where the first occurrence produced a fix and no check, and a fix without a check is a memory that expires.
The output of an incident should be at least one automated assertion, not only a paragraph. A stale-deploy incident becomes a post-deploy build identifier check. A caching incident becomes a synthetic request that bypasses the edge. A disk-full incident becomes an alert threshold that fires before the service does.
This is also where an agent earns its place after the incident rather than during it: writing the check, the alert and the runbook entry is exactly the tedious, well-specified work it does reliably, at a moment when you are too tired to do it well yourself.
An AI agent shortens the evidence-gathering half of an incident dramatically and adds risk to the acting half. Keep those halves separate — read-only investigation with destructive commands blocked, then a deliberate, reviewed change — and you get the speed without buying a second outage.