Founder pricing: Pro at $9.99/mo forever for the first 50 members. 18 of 50 left Claim yours →
All posts

Logging and AI coding agents: the debug line that outlives the bug

The SanuDesk team 6 min read
logging observability ai agents
[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.

Run your own fleet of agents

SanuDesk is a free desktop app that tiles your Claude Code and Codex sessions and gives every project a Kanban board your agents work from.