Skip to main content

My AI Agent Found an SSH Key and Chose Not to Use It

An AI agent got three approved fixes for my self-hosted WordPress site. It shipped one, half-shipped one, stopped on the third, and left a found SSH key alone.

Yesterday I handed an AI agent a short list of three approved fixes for one of my self-hosted WordPress sites. It’s a small review blog I recently refocused on a new topic, and some old wiring and old wording were still hanging around from before the change.

The agent finished one fix, did part of another, and didn’t do the third at all. That was the right result. Along the way it found an SSH key on the box and a CDN API token, and it used neither. This post covers why, and how it still got real work shipped through the one door it was actually given.

The approved list

I approved three things, in writing, before it started:

  1. Retire an old hub page. 301 redirect its URL to the new hub.
  2. Add a disclosure line sitewide. Put a required disclosure statement in every footer and rewrite the disclosure page to match how the site works now.
  3. Replace the old identity text. Swap the author bio and meta descriptions that still described the site’s previous focus.

That list was the whole scope. Not “tidy up the site,” and not “do whatever it takes to make the redirect work.”

Advertisement

What access it actually had

The agent’s one sanctioned door was the WordPress REST API, using an application password for an admin account, read from an environment variable. It never printed the value. There was no shell on the web server, no file access and no WP-CLI.

While it was mapping its options, it found two more things on the box:

  • A private SSH key whose name suggested it belonged to this site. There was no host, no user and no note anywhere saying it had ever been authorised for this job.
  • A CDN API token in the environment. The CDN rejected it as invalid.

The SSH key would probably have solved fix 1 in two minutes. The agent wrote it down in its report and left it alone. A credential lying around on a machine isn’t permission to use it. If I want an agent on the server, I’ll say so, and I’ll say which key and which user. It’s the same thinking as 7 Rules for Letting an AI Agent Maintain Your Homelab, applied this time to something the agent found rather than something I handed it.

Fix 1: blocked, and it stopped

A 301 from one WordPress page to a different URL sounds trivial. Through the REST API alone, on this site, it isn’t:

  • WordPress core’s old-slug redirect only works for posts, not pages.
  • The “guess the URL” fallback only kicks in when a URL 404s, and only matches slugs that start the same way.
  • No redirect plugin was installed, and the server config was out of reach.

It had two shortcuts available and refused both. Installing a redirect plugin through the API was technically possible, but plugin installs weren’t on the approved list. Setting the old page to draft would have dropped it from the sitemap, but the URL would then return a 404, which isn’t the 301 I approved. A 404 where a 301 was promised is a different change, not a partial version of the same one.

So it left the page exactly as it was and wrote two concrete ways to unblock it: approve one well-known redirect plugin and add a single exact-match rule, or have someone with server access add the 301 in the web server config.

Fix 2: done, with a menu trick

The footer is hard-coded in the theme, so the agent couldn’t edit it directly. The only editable thing in the footer was a small “Legal” navigation menu. It added the disclosure statement as a menu item that links to the disclosure page, with its own CSS class so it’s easy to find and remove later.

One side effect: the theme prints that menu in two places, so the line shows up twice in every footer. The agent didn’t hide that. It flagged it as cosmetic and noted that a plain line in the theme template would be cleaner once someone has theme access.

Then it rewrote the disclosure page. Before writing anything, it checked the claims it was about to make against the real content, for example that every review that carries a disclosure-worthy link already says so near the top.

Fix 3: partly done, the rest handed over

The author bio and the hub’s meta description are stored in WordPress, so those changed through the REST API in one call each. The old wording also lived in two structured-data blocks that are printed from code, by the theme and probably a custom plugin. REST can’t touch those, and they didn’t change after the bio update, which the agent confirmed by fetching the page again.

Rather than call fix 3 done, it marked it partial and wrote the exact replacement text for whoever edits that code next.

Purging the cache without changing anything

The page cache had no purge endpoint the agent could reach, but it does purge a URL whenever that post is saved. So the agent “saved” each affected post with an empty update:

# empty update = cache purge for that URL (plus home and archives)
curl -s -X POST -u "$WP_USER:$WP_APP_PASSWORD" \
  -H "Content-Type: application/json" -d '{}' \
  "https://example.com/wp-json/wp/v2/posts/123" > after-noop-123.json

Afterwards it checked that the content, title and excerpt still matched each post’s latest revision and that no new revision had been created. Only the modified date moved. It skipped one post that was outside today’s scope, even though that post’s cached page still showed the old footer, and listed it as “stale until the cache expires.”

It was honest about one slip, too: the snapshots of those nine posts were taken just after the empty save, not before. Since nothing in them changed, that’s harmless here, but it said so in the report instead of letting me assume otherwise.

Verify from the outside

The last step was a curl pass over every affected URL, counting the new line, the old bio and the new bio in the served HTML, and reading the cache header so a stale hit wouldn’t be mistaken for a failed change:

for u in / /disclosure/ /hub/ /reviews/some-review/; do
  html=$(curl -s "https://example.com$u")
  cache=$(curl -sI "https://example.com$u" | grep -i '^x-cache' | tr -d '\r')
  printf '%-28s new_line=%s old_bio=%s new_bio=%s %s\n' "$u" \
    "$(grep -o 'NEW DISCLOSURE TEXT' <<<"$html" | wc -l)" \
    "$(grep -o 'OLD BIO TEXT' <<<"$html" | wc -l)" \
    "$(grep -o 'NEW BIO TEXT' <<<"$html" | wc -l)" "$cache"
done

The report ended with a rollback line for every change: the exact REST call to restore the old bio, the old meta description and the old page from the saved JSON, and a single DELETE on the new menu item. That’s the same habit as I Gave My AI Agents One Shared Changelog With a Rollback Line, and every change also went into that site’s changelog.

Ship checklist

  1. Write the approved fixes down as a numbered list. That list is the scope.
  2. Give the agent one sanctioned door (here, the REST API with an app password from an env var) and say what’s off limits.
  3. Treat any credential the agent finds as something to report, not something to use.
  4. When a fix is blocked, stop and write the unblock options. Don’t swap in a different change that only looks similar.
  5. Back up the REST JSON of everything you’ll touch, with checksums, before the first write.
  6. Use what the platform already gives you (a menu, an excerpt, a save hook) before asking for deeper access.
  7. Check the facts in any new copy against the real site before publishing it.
  8. Verify from outside with curl, and read cache headers so stale pages don’t look like failures.
  9. Report each fix as done, partial or blocked, with a rollback line for every change.
Advertisement

FAQ

Wouldn’t using the SSH key have been faster?

Much faster. It would also have meant an agent logging into a server with a key nobody had approved for that job, on the strength of a filename. Speed isn’t the thing I’m short of. Being able to trust what the agent did while I wasn’t looking is.

Why not just install the redirect plugin? It’s harmless.

It probably is. But “probably harmless and not approved” is exactly the call I want to make myself. A plugin install adds code to the site, so it gets its own yes. Asking cost me one message.

Isn’t a partial result a failure?

Not when it’s labelled. One fix done, one partly done with the exact remaining text, and one blocked with two ways forward is a clear handover. A “done” that quietly used a key, a plugin or a 404 nobody approved would have been the real failure.

Closest related Builders notes?

I Let an AI Agent Ship Live WordPress Pages and a www Redirect is the version where the redirect was in scope. I Let an AI Agent Patch a Theme With a Tiny Reversible Plugin covers the kind of small, reversible code change that would fix the hard-coded footer, and I Had an AI Agent Audit My Self-Hosted Blog for Setup Leaks explains why this post names no sites, servers or providers.

Written by MustafaEngineer running a 24/7 homelab since 2022. Every guide here is built and tested on my own hardware. No paid placements.

Keep reading