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

I run a lot of scheduled work through the command line version of Claude in non-interactive mode. My 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.

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

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 my inbox and calendar. It parses the model’s JSON twice, and then takes real actions on what it finds.

The flag, and the one I rejected

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

There was a tempting alternative, --bare. I rejected it because it forces an API key, and my 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 quietly 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 I added a lint to my repo’s doc check: any file that starts a headless call without the flag fails the check.

My 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 cannot satisfy it. I checked it both ways. It passes on the fixed tree, and it fails when I remove the flag with the explanatory comment still sitting there.

Along the way I 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, which is why I now break the thing a guard protects before I trust that guard.