Skip to content

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

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 *.pem last 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 JSON hooks.json config lives under the Cursor directory.
  • Cursor reads hooks.json only 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 HOME is unset; refuse to run without piped stdin so a stray manual invocation cannot hang on cat.

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/beforeReadFile dominance are Elastic's observed fleet statistics, not universal guarantees; treat them as calibration, not thresholds.

Sources