
Fifteen years building engineering platforms, currently focused on advanced AI infrastructure at yeet. I love turning the deeply complex topics into something everyone can understand. I relate deeply with the core yeet philosophy that you can just build things.
Last updated: October 2026
Quick answer. Yes, for a large class of problems. An agent can read every statement your application sends Postgres or MySQL off the socket, with the values the driver bound, without a role, a password or a connection to the database. That covers N+1 patterns, query storms, unexpected statements from a service that should not be issuing them, and which process sent any of it. What it cannot do is explain a query plan, because whether an index was used exists only inside the database and
EXPLAINis the only thing that knows. The useful split is that what your application sent is observable from your own host, while what the database did about it is not.
I build eBPF tooling at yeet, so I spend a lot of time on the question of what can be observed without being granted anything. I am not going to tell you whether your security team should approve an agent touching production, and I have no view on your schema. The claim I want to make precise is narrower and more checkable: a read-only guarantee that comes from a tool having no write path is a different kind of assurance than one that comes from a GRANT, and the difference matters most in exactly the situation where people are most nervous.
Because a connection is a capability, and capabilities granted to an agent are harder to reason about than the same capabilities granted to an ordinary program. A conventional script does a fixed thing with its connection, and you can read the script. An agent decides what to do with its connection at runtime, based on a model's judgment about a prompt, and the set of statements it might issue is not enumerable in advance.
That is the structural worry, and the usual mitigations address it only partly. A read-only role bounds writes but not reads, so every row the role can see is a row the agent can retrieve and put into a context window that leaves your machine. Statement timeouts bound duration, not disclosure. A separate replica bounds blast radius to a copy of the same data. Each is reasonable and none of them makes the underlying question go away, because the thing you granted was the ability to ask the database for data.
There is a second worry that is less discussed and more mundane: the connection has to be configured. Someone creates a role, puts a password somewhere the agent can read it, and that credential now exists on disk, in an environment variable, or in an MCP server config, where it outlives the debugging session that justified it.
More than most people expect, because the statements themselves are the evidence for a surprising share of real problems. The reflex is to treat the database as the only authority on queries, when half of what you want to know is a fact about your own application that never required the database's cooperation to observe.
Every query your application sends leaves the process as bytes on a socket before the database ever sees it. A kernel probe on the socket write path reads those bytes and parses the wire protocol, which means the agent gets the statement text, the bound parameter values, the timing, and the identity of the sending process. sqlsnoop is the yeet script that does this for Postgres and MySQL.
The problems that become visible from traffic alone are the ones where the pattern, rather than any single query, is the bug:
tcp_sendmsg sees the bytes the driver wrote, so customer_id = 4471 is an observation rather than a reconstruction from a prepared-statement template.Point three deserves emphasis because it is the thing a database-side tool structurally cannot do. pg_stat_statements aggregates by statement text and does not record the client. The usual workaround, reading application_name from pg_stat_activity, stops identifying the real sender as soon as a connection pooler shares connections between services. Behind PgBouncer the sender is not recoverable from inside the database, and it is free from the socket.
Yes, and the reason is worth separating from the usual version of this claim, because "read-only" is doing different work in the two cases. One is a rule the database agrees to apply, and the other is the absence of any mechanism capable of writing. Those sound similar in a policy document and behave differently when something goes wrong.
A read-only database role is a policy decision enforced by the database. It is reliable, and it is also a configuration that someone can change, that has a privilege escalation surface, and that depends on the role being set up correctly in the first place.
A socket reader has no write path at all. There is no connection, no session, and no statement it is capable of issuing, so there is nothing to misconfigure in a way that grants writes. The tool's own documentation states the limit directly: it "is observability, not enforcement," and it "reads bytes and never modifies, delays, or blocks a statement." The matching database-alerts prompt carries the same constraint into the agent's instructions, telling it to watch only and "never change my databases, their configuration or their users."
That is a property of the mechanism rather than a promise about behavior, which is the strongest form this kind of assurance takes. It does not depend on the agent being well-behaved, on the prompt holding, or on a role being configured correctly.
Yes, and anyone presenting this approach as a privacy win without saying so is overselling it. The credential question and the data-exposure question are separate, and solving the first does not touch the second. Teams that conflate them end up with a setup that is easier to approve than it is to justify.
Query traffic contains bound parameter values, and those values are frequently personal data. A WHERE email = ... carries an email address. Capturing real values is exactly what makes the tool useful for debugging, because a reconstructed template tells you the shape of the query and not which row was slow, and it is also what makes the captured traffic sensitive.
The accurate description is that the exposure has moved rather than disappeared. The agent no longer holds a credential capable of reading every row in the database, which is a real reduction, and it does see the values flowing through the statements it watched during the window it was running. For most debugging sessions that is a good trade. For a database where any row is regulated, treat the capture the way you would treat an application log that prints parameters, because that is what it is.
Two details bound the exposure in practice. The capture is held in memory, 2,000 statements on one host with no retention and no aggregation across machines, so quitting ends it. And statements are read into a fixed window, 256 bytes for the statement and 192 for the parameter block, so a long statement shows its opening rather than its full contents.
Anything whose answer lives in the database's own state, and the line between the two categories is clean rather than fuzzy. Once you have sorted a question onto the correct side of it, which tool to reach for stops being a judgment call.
| Question | Answerable from traffic | Needs a connection |
|---|---|---|
| What statements did my app send, how often, from which process | Yes | |
| Which parameter values were bound | Yes | |
| Is this query using an index, what is the plan | EXPLAIN ANALYZE | |
| How large is this table, how bloated is this index | Catalog queries | |
| What is holding this lock right now | pg_locks | |
| Which statements are expensive across all clients over weeks | pg_stat_statements |
Read the right-hand column and a pattern emerges: it is all questions about the database's internal state and history, which the database is the only witness to. The left column is questions about your application's behavior, which your application is the witness to.
A sensible arrangement uses both and keeps the grants separate. The socket reader finds what is happening, with no credential involved. A read-only role, used deliberately, explains why the database is slow about it. Finding the N+1 does not require a credential, and deciding whether the fix is an index does.
Four limits apply, and the first rules the approach out entirely for some stacks, so it is worth checking against your own runtimes before planning around the tool rather than after. None of them is a configuration problem you can work around with a flag.
Go and Java clients over TLS are invisible. The TLS probes attach to SSL_read and SSL_write in OpenSSL. Go's crypto/tls is pure Go and Java's JSSE lives inside the JVM, so neither exposes a C symbol to hook. Plaintext connections from both are read normally. This limit is unavoidable rather than unimplemented.
Unix socket clients are invisible. The probes hook tcp_sendmsg, so a local client going through /var/run/postgresql/.s.PGSQL.5432 never appears. Connect over TCP to see it.
Latency under pipelining is approximate. Neither wire protocol carries a request id, so a reply is paired with the oldest outstanding send on its socket. That is correct for the ordered request and response cycle every normal SQL client uses, and a genuinely concurrent multiplexer over one connection would defeat it. The tool labels this socket-paired rather than presenting it as exact.
Processes that started before the probes attached. A uprobe only fires for processes that start afterwards, so a client already running when you attach stays invisible on the TLS path until it reconnects. Start the capture before the workload.
The same way you run it anywhere, which is the main practical advantage of attaching at the socket. The probes live on your application host rather than on the database, so a managed instance you have no superuser access to and cannot restart stops being an obstacle to seeing your own queries.
curl -fsSL https://yeet.cx | sh # install yeet, once
yeet run gh:yeet-src/sqlsnoop # watch every Postgres and MySQL client on the host
Nothing is installed into the application, no driver is wrapped, and no redeploy is involved. Compare that with the database-side alternatives on a managed instance: log_statement = 'all' needs a configuration change and generates log volume proportional to traffic, and pg_stat_statements is a shared library requiring a restart, which you may not be permitted to perform on RDS or Cloud SQL at all.
For encrypted connections the TLS probes need a target binary, passed after -- so the flag reaches the script rather than yeet itself:
yeet run gh:yeet-src/sqlsnoop -- --tls-binary libssl.so
yeet run gh:yeet-src/sqlsnoop -- --tls-binary "$(command -v node)"
A large share of database debugging is really application debugging, and application behavior is observable from the host the application runs on. Reading statements off the socket answers those questions with no credential, no connection and no reconfiguration, which also makes it the approach that survives a managed instance and a connection pooler. Keep a database connection for the questions only the database can answer, and grant it when one of those questions actually comes up.
Yes, for questions about what the application sent. A socket-level reader parses the Postgres and MySQL wire protocols from the sending process, so no role, password or connection is involved.
Yes. N+1 is a count of statements by shape over a window, which is fully visible in traffic leaving the application. The database connection is not what makes the pattern detectable.
It removes the credential and the ability to query arbitrary rows, which is a real reduction. It does not remove exposure to the parameter values in the statements it watched, so it moves the risk rather than eliminating it.
No. It has no connection and issues no statements, so there is no write path to misconfigure. That follows from the mechanism rather than from a permission setting.
Yes, because it attaches to your application host rather than the database instance. That is the main advantage over pg_stat_statements, which needs a restart you may not be able to perform.
No. A query plan exists only inside the database, and EXPLAIN requires a connection. Whether an index was used is invisible from the wire.
No. The probes run on the application host and read bytes already being written, so any cost lands there rather than on the database server.
Over OpenSSL, yes, with a --tls-binary target. Go's crypto/tls and Java's JSSE do not expose a hookable C symbol, so TLS traffic from those runtimes cannot be read.
No. The probes attach from outside the process, so no instrumentation library, driver wrapper or redeploy is required.
It is held in memory, 2,000 statements for one host, with no retention or cross-machine aggregation. Quitting the tool ends the capture.
sqlsnoop, the eBPF SQL statement monitor described here, including its supported databases and documented limits