Logging and AI coding agents: the debug line that outlives the bug
[2026-08-03 09:14:07] production.DEBUG: registration attempt {"name":"Alex Rossi","email":"alex@northfield.dev","password":"h4ckMe!2026"}
An agent wrote that line eight days earlier, chasing a bug where new
signups sometimes failed validation with no clear reason given. One
Log::debug() call went into the registration controller, a failing
request came through with a trailing space in the confirmation field,
the mismatch was obvious, and the fix took two minutes. Nobody asked
the agent to remove the line afterward. Taking it out was never part
of the task.
Nobody wrote a bug. The agent logged exactly what it was told to inspect, and the inspection happened to include a password.
Why a debug line that helps in dev becomes a liability in production
Locally, you watch it happen: Laravel Pail tails the file, you read the line that matters, fix the bug, and Ctrl-C. Nothing about that workflow says what happens to the same call once nobody is watching it.
In production, the same call runs on every request that touches that
code path, not once. Whether anyone reads the result depends on
LOG_LEVEL, and Laravel's log levels gate the channel, not the call
site: those RFC
5424 severities decide what a channel writes, not whether the line
exists in your code. A fresh Laravel install ships LOG_LEVEL=debug
by default, and this project's own .env.example still does. Nothing
about deploying to production changes that value unless someone edits
it. It lives in a file an agent chasing a validation bug has no reason
to open.
Retention is the second gap. The daily channel driver rotates and
prunes old files on a timer. The single driver, Laravel's default
local channel, just appends forever: no rotation, nothing pruned. A
debug line written into a file with no expiry doesn't need a second
bug to become permanent, the same way a queued job that runs more
than once didn't need one
either.
Two Laravel features built for this, and their exact gap
Two features exist specifically to make logging safer, and each promise is narrower than it sounds.
Log levels protect the sink, not the call site. A message written
at debug gets filtered out by any channel with its threshold raised
above it. It does nothing for the call itself: the line still runs,
still sitting there for the next person who lowers the threshold and
finds months of old debug output already waiting.
Context makes every log line richer, and every log line inherits
it. Laravel's Context facade lets a request, job, or command
attach data once and have it included in every log message written by
your application for the rest
of that lifecycle: a trace ID added at the start of a request shows up
on every line it produces, with nothing threaded through by hand.
That's by design. It's also why one Context::add() call, written to
solve a debugging problem, rides along on every later log call
in that request, including ones nobody added it for and nobody is
thinking about.
Neither feature is broken, the same gap this blog keeps finding
between a guardrail and what it doesn't
cover: real protection, narrower
than the name suggests. An agent reaching for Log::debug() has no
reason to check either one. The line runs, the bug becomes visible,
and the task reads as done.
A different leak than a committed key
This blog has already covered how a credential gets out through a
commit, a terminal echo, or an injected
instruction. This is a
different path: nobody has to run cat .env. The data leaves through
a log line an agent added on purpose, to do the job it was asked to
do.
Say a second agent, weeks later, builds an unrelated admin screen so
support can look up a signup by name or email. It reads from wherever
that data already lives: the log file, or whatever it forwards to, a
papertrail drain, a slack channel, a viewer wired into the admin
panel for some other reason. Neither task mentioned passwords. The
first agent logged a field it was told to inspect. The second built
exactly what it was asked to build, on a source nobody had flagged as
sensitive.
It's the same evidence-gathering instinct this blog has recommended before: hand an agent your logs so it can observe instead of guess. The logs it reads from are the same ones it keeps writing to.
Where logging goes wrong, and what it really costs
"It only fired once in my test" describes the call, not its lifetime. A local run shows you one line, once, for a request you controlled. Production runs the same call on every matching request, indefinitely, for requests you will never read individually.
A debug line doesn't need sensitive data to be expensive. A
Log::debug() call inside a loop or a queue job is disk writes, and
once it reaches a hosted sink, billed volume. Cost doesn't require an
incident, only traffic.
Rotation is not the same as gone. The daily channel prunes its
own files after a set number of days. Anything already forwarded to
Slack or a third-party drain left that window the moment it was
written. Deleting the original does nothing to the copy.
The discovery is almost always an accident. Facebook's own account of its 2019 plaintext password incident is the extreme version of this shape: internal applications had been logging unencrypted password data since 2012, searchable by more than 20,000 employees, found seven years later when security engineers reviewing unrelated code noticed the pattern. Nobody was looking for exposed passwords. Nobody had to be, for the line to keep running.
Read what your agent logs before it ships
SanuDesk is a desktop app built around exactly that gap between a
debug line that helped and one that's still running months later.
Your Claude Code, Codex and Gemini sessions tile into one grid, so a
Log::debug() added to chase a validation bug is something you read
beside the diff that fixed it, not something you rediscover by
grepping storage/logs later. Work arrives through a Kanban board:
"fix the registration validation bug" is a card, and before it moves
to done, someone can ask whether this line comes out or stays. Because
SanuDesk is bring-your-own-model, that check can run on a cheaper
model than the one that wrote the fix. A recurring sweep, grepping the
codebase for Log::debug and dd( calls left in application code,
becomes a Loop, journaled per run instead of a cleanup everyone means
to schedule. The grid and board are in the free plan (see
pricing).
Grep your own logs before the next debug line ships
Pick one production log file, or your hosted drain's search bar, and
run one query right now: password, token, secret,
authorization, case-insensitive. Then check LOG_LEVEL in the
environment file that actually runs production, not the example one.
If either turns up something you didn't expect, that is the fix to
make today, before the next debug line ships and outlives the bug it
was written for.
Download SanuDesk free to give every debug line a reviewer before it ships, or see how the grid, the board and Loops fit together on the features page.