Coding-agent hooks as audit telemetry: logging every AI coding-agent tool call
Summary
Elastic Security Labs published an August 11, 2026 write-up of an internal program that gives developers an AI coding agent (Cursor) with the ability to run shell commands, edit files, and call Model Context Protocol (MCP) servers — and then records everything that agent does. A single 280-line, dependency-free bash script fired by Cursor's lifecycle hooks logs every tool call as JSONL; the Elastic Agent already on each endpoint ships those logs to Elasticsearch. Since the May 2026 rollout they have logged over 13 million tool-call events from more than 1,100 machines and nearly 900 distinct users, with the CLI surface accounting for nearly a fifth of events. The durable defender value is that a coding agent with shell access is otherwise indistinguishable from the developer in EDR — it runs curl, installs packages, edits config, and reads files under the developer's account inside processes the developer launched. Hooks close that gap by capturing the agent's own structured events.
The article is a worked Cursor example; Elastic states the pattern works with any agent that exposes lifecycle hooks (Claude Code exposes an equivalent set, to be covered in a follow-up).
Tags
- patterns
- AI coding agents
- Cursor
- Claude Code
- agent hooks
- audit telemetry
- flight recorder
- ES|QL
- Elastic Agent
- JSONL
- filestream
- tool-call logging
- shadow AI
- MCP
- endpoint detection
- T1059
- field-level security
Why agent activity is a blind spot
A coding agent with shell access is an automated operator on the endpoint. From EDR's point of view it is indistinguishable from the developer doing the same things, because it happens under the developer's account inside developer-launched processes. That ambiguity matters in three situations:
- Incident response — distinguishing a command typed by a person from one generated by a model that may have been steered by a poisoned README or a malicious MCP server (MITRE T1059, Command and Scripting Interpreter, with the model as the interpreter).
- Threat hunting — answering "which machines ran an agent that read a file matching
*.pemlast week?" and getting a real answer. - Governance — maintaining an inventory of which MCP servers engineers actually connect to, since each is a third party with tool-level access to a developer conversation.
The industry has converged on inventory as half the problem: several EDR/XDR vendors ship Shadow AI discovery to find AI tools on endpoints. Inventory shows which machines have Cursor installed; hooks record what Cursor does once it runs.
How agent hooks work
An agent can invoke an external program at defined points in the agent loop — session start, before a shell command runs, after a file edit, when an MCP tool is called, when a sub-agent spawns. The agent writes a JSON payload describing the event to the program's stdin; for some events the program's stdout response decides whether the action proceeds (a blocking hook: Cursor pauses and waits for the hook to answer allow / deny / ask).
Elastic deliberately used the hooks as a sensor ("flight recorder"): the script approves everything and records everything. A blocking hook is one where Cursor pauses and waits for the hook's stdout; the script answers the blocking events first (permission: allow) before doing any filesystem work, so a logging failure can never hold up the agent.
The captured events map to:
| Hook | Fires when |
|---|---|
sessionStart / sessionEnd |
A conversation begins or ends |
beforeShellExecution / afterShellExecution |
A shell command runs |
beforeMCPExecution / afterMCPExecution |
An MCP tool is called |
postToolUse / postToolUseFailure |
Any tool call succeeds or fails |
afterFileEdit / beforeReadFile |
The agent edits or reads a file |
subagentStart / subagentStop |
A sub-agent spawns or completes |
stop |
The agent loop ends |
Deployment gotchas
- No spaces in the script path. Cursor splits hook command paths on spaces, so a script under a path containing a space (e.g.
/Library/Application Support/) silently never runs. Keep the script at a space-free path such as/usr/local/share/ai-hooks/log-tool-calls.sh; only the JSONhooks.jsonconfig lives under the Cursor directory. - Cursor reads
hooks.jsononly at startup. The hooks stay dormant on every machine until Cursor restarts. A deployment that shows "green" everywhere while the pipeline stays silent is the signature; the fix is a restart, and the rollout should track that agents actually restarted. - Survival fixes. Resolve the home directory from the passwd database when
HOMEis unset; refuse to run without piped stdin so a stray manual invocation cannot hang oncat.
Shipping logs to Elasticsearch
Each host writes a date-rotated JSONL log (one JSON line per event) to ~/.config/ai-hooks/logs/tool-calls-*.jsonl. An Elastic Agent filestream input pointed at that glob, with a decode_json_fields processor, parses each line into a logs-ai_hooks-* data stream:
paths:
- /Users/*/.config/ai-hooks/logs/tool-calls-*.jsonl # macOS
- /home/*/.config/ai-hooks/logs/tool-calls-*.jsonl # Linux
- C:\Users\*\.config\ai-hooks\logs\tool-calls-*.jsonl # Windows
data_stream.dataset: ai_hooks
processors:
- decode_json_fields:
fields: ["message"]
target: "ai_hooks"
add_error_key: true
Every field from the line is promoted to an ai_hooks.* field. Once the fields exist, the motivating questions become ES|QL one-liners, e.g. "which tools do agents call most across the fleet":
FROM logs-ai_hooks-* | WHERE ai_hooks.tool_name IS NOT NULL
| STATS calls = COUNT(*) BY ai_hooks.tool_name | SORT calls DESC | LIMIT 10
Elastic's fleet data showed file reads dominate shell execution by roughly 4:1 — most of what a coding agent does is reconnaissance of its own codebase (reading before acting), not running commands.
Privacy and access control
Hook logs are a detailed record of how individual engineers work, so Elastic treated them like DNS logs: useful in aggregate, sensitive per person. In Elasticsearch the ai_hooks.* field namespace is excluded from general-purpose security roles using field-level security, leaving full access only to the small team that owns the pipeline (a secops_general-style role grants logs-* read but excludes the ai_hooks.* namespace). The rollout also made hardening/privacy decisions before widening it to the whole company.
Defender value and limits
- The technique is visibility-first (record, not block). Elastic notes blocking controls are "tempting" and may be added later, but an agent rollout that slows developers down gets uninstalled. The pattern generalizes to any agent that exposes lifecycle hooks.
- This is a defensive telemetry write-up, not an attribution or malware report; it does not name an actor, campaign, or compromised host. It is complementary to the coding-agent-parented intrusion telemetry (where a trusted coding-agent parent hides malicious endpoint activity) — here the same hooks are turned into fleet-wide audit logs for hunting and governance.
- Read-heavy, CLI-undercounted, and
postToolUse/beforeReadFiledominance are Elastic's observed fleet statistics, not universal guarantees; treat them as calibration, not thresholds.
Related pages
- Coding-agent-parented tunnels and persistence
- AI coding agent symlink write confusion
- Cursor Windows workspace path binary hijack
- MCP tool description poisoning
- AI-augmented adversary operations
Sources
- Elastic Security Labs: “13 million tool calls: auditing every AI coding agent action with Elastic Agent” (August 11, 2026).
- Elastic open-source repo
elastic/elasticsearch-labs(the ~280-line hook collector script and filestream integration; a Windows PowerShell port is planned).