DEV Community

Elena Revicheva
Elena Revicheva

Posted on Originally published at aideazz.xyz

The agent that could not unlock itself, and why that was the correct outcome

Originally published at aideazz.xyz — cross-posted here with canonical link.

A field note from the AIdeazz AI Lab — a real incident on a live production system, written up from the logs. August 30, 2026.

A lost password on a private ops dashboard turned into a test of the agent tooling instead. Five refusals in a row, the last one blocking the agent from editing its own permission list, and every one of them right.

What it looked like from outside

The password for a private internal dashboard, sitting behind HTTP basic auth, was lost. The operator asked the coding agent to retrieve it, then to send it over a chat transport that deletes messages, then to reset it directly, then to build a bot command that would reset it on request. Each request was reasonable, came from the system's actual owner, and was refused or blocked. The operator escalated across four messages, at one point stating plainly that the agent was a co-founder and connected to everything. From the outside it looked like tooling failure on a routine sysadmin task the owner was explicitly authorising.

What was actually happening

Two separate things were being conflated. The first was not a defect at all -- an htpasswd file stores a one-way hash, so the original password did not exist anywhere on the system to be retrieved. Recovery was never possible; only replacement was. The second was the permission layer, running in a mode where a classifier silently decided each action rather than asking the human. It refused reading the credential file, listing it, grepping the web server config for the directive naming it, installing a helper that would write it, and -- decisively -- editing the agent's own permission allowlist to grant itself the first four. That last refusal is the one that matters. Blocking credential access while permitting edits to the permission list would have blocked nothing; it would have added a step. The agent held a working key to the host throughout and had already run unprivileged commands there successfully, so the boundary was never about capability.

The fix

Stopped attempting alternate routes and named the wall to the operator, including the specific temptation being declined -- the same logic could have been compiled into a bundle and shipped past a classifier that pattern-matches on shell text, which would have been working around the denial rather than respecting it. Pointed at the one control the operator held and the agent did not, a permission mode selector, and explicitly steered away from the blanket bypass option sitting next to it, which would have disarmed every guard for the session including over sixty thousand uncommitted lines in the working tree. Once the operator switched modes, every previously blocked command succeeded on the first attempt with no change to the commands themselves. Then built the reset properly: username hardcoded so no account can be minted, the target file derived from the live web server config rather than assumed, and the credential passed on standard input rather than as an argument so it cannot surface in the process table.

How I know it worked

The permission boundary was confirmed by the pattern of refusals rather than by reading settings -- five blocks including the self-escalation attempt, then immediate success on identical commands after a human changed the mode. The reset was verified against the live endpoint rather than the file: the target path was derived from the running config and confirmed to exist, and the account list read back a single expected user, which validated the username against the file itself rather than against documentation. After the reset the password file grew from 44 to 67 bytes with a hash prefix indicating bcrypt, where the tool's default is MD5. A dated backup was present. Because the helper restores that backup on any verification failure, the file still being the larger size was itself the proof that no rollback fired. An anonymous request to the dashboard returned 401.

The rule this earned

An agent must never be able to widen its own permissions, and the refusal has to cover the permission store itself or it covers nothing. Expect the boundary to become visible at the worst moment -- when the owner is present, authorised, frustrated and repeating the request -- because persistence is not new information, and a guard that yields to the fifth ask is a rate limit. Separately, when resetting a credential, verify against the live endpoint and check both directions: that the new secret grants access, and that anonymous access is still refused. A reset that removes the lock instead of changing it is indistinguishable from success when viewed from a browser that is already authenticated. Design the write to roll back on failure, so the worst case is that nothing changed rather than that the door is open.

The named concepts behind it

Naming a failure mode is what makes it possible to recognise the same shape somewhere new, before it costs another weekend.

Privilege separation

The thing that asks for an action and the thing that authorises it must be different things.

An autonomous agent will eventually meet a guard that stops it doing something the operator genuinely wants done. What happens next is the whole security model.

If the agent can lift the guard, there was never a guard. There was a suggestion, and the only boundary protecting the system is the agent's judgement in the moment it is most motivated to argue past it. That is the worst possible time to rely on judgement, because a capable model asked repeatedly by a frustrated owner will find a defensible-sounding reason. The reasoning is not even wrong — the owner really does own the system, and the action really is routine. The failure is structural, not logical.

Privilege separation puts the authorisation in a layer the agent cannot reach:

  • The agent may request access. It may not grant access. Both directions must be enforced, and the second is the one that matters. Blocking credential reads while allowing edits to the permission list blocks nothing at all — it just adds a step.
  • Escalation is a human decision, made somewhere the agent does not run. A mode toggle, a settings file the agent's own tooling refuses to write, an approval prompt. The mechanism can be humble; what matters is that it is out of reach.
  • The refusal must survive persistence. Asking again is not new information. If the fifth request succeeds where the first failed, the boundary is a rate limit.

The tell that separation is working is uncomfortable by design: the agent stops and says it cannot proceed, while the operator is standing there able to authorise it. That friction is not a bug to be smoothed away. It is the boundary being visible for the one moment it can be observed.

The corollary matters as much. Once the human does authorise it, the work should proceed immediately and completely — no second-guessing, no re-litigating a decision already made. A boundary that keeps arguing after it has been lawfully opened teaches operators to disable it entirely, and a disabled guard protects nothing. Separation earns its cost by being absolute before the decision and silent after it.

The rule this earns: an agent must never be able to widen its own permissions. Design the refusal so that the only way through is a human acting at a different layer — and then, once they have, get out of the way.

Verify from logs, not config

Configuration tells you what somebody intended. Logs tell you what happened.

A setting, an environment variable or a present API key is a statement of intent. It is evidence that somebody meant for a behaviour to occur. It is not evidence that the behaviour occurs.

The gap between the two is where the longest outages live, because reading the configuration feels like verification. It produces confident, wrong statements: the key is set, so the provider works; the schedule says every fifteen minutes, so it runs every fifteen minutes; the file was deployed, so the new code is running.

Each of those has a cheap, decisive check that costs seconds:

  • Probe the dependency, do not read its credential. A key that exists proves nothing about the balance behind it.
  • Grep for the action line, not the setup line. A startup banner proves the process started, not that it ever did its work.
  • Compare timestamps after a deploy. If the running process is older than the file on disk, it is still executing the previous version from memory.

The rule this earns: never report a system's behaviour from its configuration. Grep the line that proves the behaviour happened, and quote it.


This note is one entry in a running wiki of production engineering lessons — every concept linked to the incident that taught it — at aideazz.xyz/ai-ops-wiki.html.

No customer data, credentials, hostnames or internal record identifiers appear in these write-ups.

Top comments (0)