Security Engineering

Auditing Agent Tool Calls: Logging That Survives an Incident

David Guzenburg/ / 8 min read

Every agent incident ends with the same question — what did it actually do? Without a log the honest answer is that you cannot find out.

audit loggingdetectionincident responseobservability

You cannot investigate what you did not record

Most incidents involving agents are discovered late and indirectly — a credential appears somewhere it should not, a commit contains something nobody remembers writing, a bill spikes. The question that follows is always the same: what did the agent actually do?

Without logging, the honest answer is that you do not know and cannot find out. Transcripts, if retained at all, are usually local, unstructured, and hold the conversation rather than the actions.

What to record

Six fields cover most investigative needs.

FieldWhy
Timestamp, UTCCorrelation with other systems
Session IDGroups calls; establishes what else was in that context
PrincipalWhich human or service account the session ran as
Tool name and serverWhat was invoked, and from which server
ArgumentsThe part that answers "what did it actually ask for"
Outcome and sizeSuccess or failure, and how much came back
{"ts":"2026-07-13T09:14:02Z","session":"s_8f2a","principal":"svc-agent-ci",
 "server":"github","tool":"create_pull_request",
 "args":{"repo":"acme/api","base":"main","head":"fix/4471"},
 "result":"ok","bytes_out":1204}
{"ts":"2026-07-13T09:13:41Z","session":"s_8f2a","principal":"svc-agent-ci",
 "server":"filesystem","tool":"read_file",
 "args":{"path":"src/auth/session.py"},
 "result":"ok","bytes_out":8231}

One JSON object per line, appended. Nothing sophisticated is required, and the format matters less than the discipline of writing it somewhere durable.

Where the log lives

If the agent can write to its own audit log, the log is evidence of nothing. Ship it off the machine — to a logging service, a separate host, an append-only store. A log the subject can edit is a log you cannot rely on in exactly the situation you need it.

Arguments are the interesting field

Tool names alone tell you little. read_file appearing four hundred times in a session is normal. read_file on ~/.aws/credentials is not, and only the arguments distinguish them.

This creates a tension. Arguments are also where secrets appear, so a log capturing them verbatim becomes a secondary store of sensitive data. Two ways to resolve it, and you can use both:

What to alert on

Logs nobody reads are archaeology, not detection. Four rules cover a lot of ground and produce few false positives:

RuleWhat it catches
Access to known credential pathsReads of ~/.ssh, ~/.aws, .env, key files
Outbound to a non-allow-listed hostExfiltration attempts, and misconfiguration
Irreversible action from a tainted sessionThe workflow-splitting rule, enforced
Tool called that this session has never usedSudden capability changes mid-session

The third requires that you record whether a session is tainted, which means recording when untrusted content entered. That is one extra boolean and it converts a policy people are meant to follow into one you can verify.

Reconstructing a session afterwards

The test of a logging setup is whether you can answer a specific question weeks later. Take a realistic one: a credential appeared in a public gist on the 14th — did an agent touch it?

# 1. Any session that read a file matching credential patterns
jq -r 'select(.tool=="read_file")
       | select(.args.path | test("\.env|credentials|\.pem|id_rsa"))
       | [.ts, .session, .principal, .args.path] | @tsv' agent-audit.jsonl

# 2. For a suspect session, everything it did, in order
jq -r 'select(.session=="s_8f2a")
       | [.ts, .server, .tool, (.args|tostring)] | @tsv' agent-audit.jsonl

# 3. Did that session make outbound calls after the read?
jq -r 'select(.session=="s_8f2a")
       | select(.server=="http" or .server=="fetch")
       | [.ts, .tool, .args.url] | @tsv' agent-audit.jsonl

Three queries, and you either have an answer or you have ruled agents out — which is itself valuable, because it redirects the investigation somewhere more useful instead of leaving a permanent maybe.

Worth rehearsing this once, on a session you know the history of, before you need it. Almost every logging setup has a gap that only becomes obvious when someone tries to answer a real question with it.

Sessions, not just calls

A record per tool call answers "what happened". A record per session answers "under what conditions", which is often the more useful question.

{"session":"s_8f2a","started":"2026-07-13T09:02:11Z",
 "ended":"2026-07-13T09:41:52Z","principal":"svc-agent-ci",
 "model":"provider/model-name-v4","repo":"acme/api","branch":"fix/4471",
 "servers":["github","filesystem","postgres-ro"],
 "tainted":true,"taint_source":"github.get_issue#4471",
 "calls":68,"tokens_in":184200,"tokens_out":12400}

Two fields here are worth the whole record. tainted and taint_source turn the workflow-splitting policy from something people are meant to remember into something you can query: show me every session that was tainted and then performed a write. That query either returns nothing, or it returns your next conversation.

servers is the other. Aggregated over a month it tells you which connected servers are never used — the cheapest possible input to shrinking your attack surface, since disconnecting an unused server costs nothing and removes both its capabilities and its context overhead.

Retention

Long enough to be useful, short enough not to become a liability. Ninety days covers most investigations, which typically begin days or weeks after the fact. Longer retention for the metadata and shorter for the arguments is a reasonable split — the metadata is small and rarely sensitive, the arguments are the opposite.

A shorter window than ninety days is a false economy. The gap between an agent doing something odd and anyone noticing is measured in weeks, not hours.

Making the log useful before an incident

The best argument for building this is not incident response. It is that the log answers ordinary questions nobody can currently answer.

Those questions get asked in ordinary engineering conversations, and having data rather than impressions is worth the build on its own. The security benefit arrives free, on the day you need it, which is the only day it matters.

Takeaway

Log six fields per tool call, ship them somewhere the agent cannot write, redact secrets from arguments on the way in, and alert on four specific patterns. Build it for the operational questions it answers on ordinary days — the incident-response value is the part you hope never to use.

Keep reading
Codex vs Claude

Debugging Together: You Hold the Hypothesis, It Does the Volume

Debugging is the task you cannot write a done condition for. The division of labour that works, instrumentation at a scale people will not do by hand, and defending against the coherent explanation that happens to be wrong.

Codex vs Claude

A Gateway Buys You Four Things and Costs You an Outage Surface

Per-user attribution across providers, spend caps that actually refuse, a request log on your terms, and model substitution in one place. Against that: tier-one infrastructure, a fail-open decision, and logs more sensitive than your source repository.

Codex vs Claude

Fewer Tool Calls: Efficiency or Missing Evidence?

A tool-efficient agent removes relevant uncertainty with the least risk and latency required for acceptance. Fewer calls are good when outcomes remain.

Security Engineering

Implicit Trust Propagation: Why Provenance Dies in the Context Window

Content loses its origin the moment it enters the context, and tool chains launder it further. The tainted-session model, and how to split research from action.

← Prompt Injection Through Tool Descriptions and Tool Output

All security engineering articles  ·  Every article