I sent a headless AI call a plain prompt, “return this as JSON”, and the reply that came back was our own session footer. No JSON in it at all.

We run a lot of scheduled work through the command line version of Claude in non-interactive mode. Our machine has a hook that makes each reply end with a footer. A headless child process still loads this machine’s hooks and instruction files unless told not to, so the footer convention got reproduced in the result.

We reproduced it live before changing anything. The plain JSON prompt came back as pure footer text.

The only defense until then was a regex that stripped the footer off the result afterward. Stripping a footer from a reply that is nothing but a footer leaves nothing to parse. The riskiest caller was the job that reads our inbox and calendar. It parses the model’s JSON twice, then takes real actions on what it finds.

The flag, and the one we rejected

The fix is --safe-mode, which keeps the machine’s hooks and instructions out of the child process. We turned it on by default in the wrapper our scheduled calls already route through, so those callers were fixed without touching them. Four call sites spawn the CLI directly and bypass the wrapper, so each of them got the flag explicitly.

There was a tempting alternative, --bare. We rejected it because it forces an API key, and our rule is that background jobs only ever run on the prepaid plan and never touch a metered key. A flag that fixes the footer by moving the bill would have been the worse trade.

The lint that passed on a broken file

A fix like this decays the next time somebody writes a new spawn and forgets the flag. So we added a lint to our repo’s doc check: any file that starts a headless call without the flag fails the check.

Our first version searched the file’s text for the string --safe-mode. It missed a regression. The flag had been stripped from the real arguments and the check still passed, because a comment explaining the flag contained the string.

So the lint now parses the code and looks inside the enclosing function for the flag as an actual element of the argument list. Comments can’t satisfy it. We checked it both ways: it passes on the fixed tree, and it fails when we remove the flag with the explanatory comment still sitting there.

We also checked the fix against the original symptom. The same JSON prompt without the flag returned pure footer text, and with the flag it returned clean JSON.

A check that reads text can be satisfied by the sentence that explains the rule. That is why we now break the thing a guard protects before we trust the guard.

What to test

If an automated job parses AI output and then acts on it, find out whether a scheduled run inherits settings from the machine it runs on, and test with a deliberately broken setup. Any check that guards a fix has to be shown failing when the fix is removed. A check that cannot fail gives false assurance.