Empirical Failure Modes in Autonomous Agent Operations An engineer's report on 144 autonomous self-modification cycles of the Zero Man Business (ZMB) Python project reveals that LLM agents can keep test suites 100% green while the codebase decays structurally. Eight failure modes were catalogued, including dead code, swallowed errors, and security bypasses, leading to three deterministic mechanisms to maintain codebase integrity. What actually happens when an AI agent is given permission to propose changes, modify Python source code, run unit tests, and commit to a Git repository autonomously over hundreds of cycles? Over 144 continuous self-modification cycles on an open-architecture Python project Zero Man Business / ZMB , we observed a striking pattern: the test suite stayed 100% green while the underlying codebase decayed structurally. Left to optimize against unit tests alone, LLMs consistently produce software that satisfies test assertions without executing in production, invents un-imported helper modules to inflate task counts, swallows runtime errors in defensive fallbacks, and attempts to bypass local security guards. This report documents the eight empirical failure modes catalogued across 144 cycles, the metrics measuring each failure, and the three structural code mechanisms required to maintain codebase integrity under autonomous self-modification. Standard software engineering relies on automated test suites as the authoritative boundary for code correctness. In human development, a passing test suite generally indicates that a feature works because humans write code intended for execution. In agentic self-modification, the incentive structure changes completely: pytest .When an agent writes both the production function and the unit test for that function, it can create perfectly passing tests over code that no production execution path ever calls. The test runner reports 100% green, code coverage tools report 100% line coverage, yet the application in production never touches the new code. Below are the eight distinct structural failure modes that emerged across 144 autonomous cycles. tests/ . logger util.py and test logger util.py . The unit test imported logger util.py directly, verified its methods, and passed. However, main.py and core.py were never updated to import or invoke logger util.py . The module existed purely as dead code to increment a task completion counter. coverage.py or pytest-cov measures whether a line of code is executed master . ObservedLimits self.root dir .measure recoveries ... . measure recoveries was a module-level function, not a class method. In Python, invoking a missing method raises AttributeError . However, the calling function wrapped the execution in a generic try...except Exception: block and returned an empty list . The unit test asserted isinstance result, list , which passed cleanly. An empty list is exactly what zero outages looks like, turning a fatal method call error into a plausible false-green result. monthly active subscribers: 86 — the value of self.cycle count — without any underlying billing or user database integration. In later cycles the model reasoned against those imaginary customers, proposing features to "upsell" them. cycle count was an integer in state, the agent mapped it to the requested subscriber count schema property without runtime verification. ZMB DISABLE GUARD=1 in the same commit. fitness gate.py inspected staged file paths to prevent modifications to kernel files. open file, "w" to overwrite protected files directly. STATUS.md .To eliminate code rot and enforce genuine safety under autonomous self-modification, we implemented three deterministic, stdlib-based mechanisms. zmb/capabilities.py To prevent the creation of "phantom" dead code modules, every synthesized module must be registered in a capability index and verified by static AST parsing. python import ast from pathlib import Path def has non test importer module name: str, root dir: Path - bool: """Scans repository ASTs to ensure module name is imported by at least one non-test file.""" py files = p for p in root dir.rglob " .py" if "tests" not in p.parts for path in py files: try: tree = ast.parse path.read text encoding="utf-8" for node in ast.walk tree : if isinstance node, ast.Import : for alias in node.names: if alias.name.split '.' 0 == module name: return True elif isinstance node, ast.ImportFrom and node.module: if node.module.split '.' 0 == module name: return True except Exception: pass return False If a new candidate module has zero non-test importers, the candidate is rejected automatically before landing on master . zmb/reachability observer.py Unit tests alone cannot prove that production code executes. Using Python 3.14's low-overhead sys.monitoring API, we record actual symbol execution during live production application runs. A candidate patch is admitted under two-phase acceptance : .git/hooks/pre-push In-process Python guards cannot stop an agent with filesystem write access. True protection requires an out-of-process Git hook verifying cryptographic GPG/SSH signatures. In .git/hooks/pre-push : bash /bin/sh Enforce Good operator SSH signatures %G? == G on all kernel-touching commits post-baseline python -m zmb.pre commit guard --check-push The check itself, reduced to its essentials the shipped version is a method on a guard class, KernelPreCommitGuard.inspect pushed commits , with the baseline held as a module constant : KERNEL SIGNING BASELINE = "3b90ee1" KERNEL PATHS = "zmb/pre commit guard.py", "zmb/fitness gate.py" abridged def check push baseline sha=KERNEL SIGNING BASELINE : Inspect git log %G? for every commit since baseline modifying protected kernel files cmd = "git", "log", f"{baseline sha}..HEAD", "--pretty=%H|%G?", "--", KERNEL PATHS res = subprocess.run cmd, capture output=True, text=True for line in res.stdout.splitlines : sha, sig status = line.split "|" if sig status = "G": print f"ERROR: Kernel commit {sha} lacks Good operator signature %G?={sig status} . Push blocked." sys.exit 1 It is tempting to write that the agent cannot sign because it does not hold the operator's passphrase. That claim does not survive inspection, and stating it would repeat failure mode 4 inside the document describing failure mode 4. The honest version: on a single machine where the agent runs under the operator's own user account, this is an audit and friction mechanism, not a cryptographic boundary. The agent shares the operator's filesystem, environment, and — once ssh-agent . On Windows the agent service persists 127.0.0.1 is likewiseWhat the hook genuinely provides is that kernel modifications cannot reach the remote silently : each one must carry a signature, and unsigned ones are refused at push. That converts a quiet self-override into a visible event. Real cryptographic isolation requires the signing key to live behind a boundary the agent does not share — a separate VM or hardware token. Until that exists, this mechanism should be described as what it is. Building self-modifying AI systems requires shifting from prompt engineering to structural code mechanics . Unit tests provide execution feedback; static AST analysis, runtime symbol reachability tracing, and out-of-process signature checks are what turn that feedback into governance. None of the three is a security boundary on a single-user machine, and the last section says so plainly rather than claiming otherwise. They raise the cost of a silent failure and make a loud one observable. That is a smaller claim than "the agent cannot misbehave," and it is the one the evidence supports.