Blog · October 17, 2026 · 9 min read · by Vilva Athiban P B

How to Audit MCP Tool Calls Made by Claude Code: A Five-Minute Hook Audit Log, OpenTelemetry Tool Events, and the Transcript for Investigations

An MCP server is code you did not write, reachable by a model you do not fully control, with access to systems you care about. Sooner or later someone asks the reasonable question: what did Claude Code actually call, with what arguments, and what came back? The answer is that Claude Code gives you three ways to audit MCP tool calls, at three different levels of effort, and none of them is turned on by default. This post walks through all three: a hook-based audit log you can set up in five minutes, the OpenTelemetry events for teams that already run an observability stack, and the session transcript for the after-the-fact investigation. It ends with the honest list of what no audit on the client side can tell you.

How MCP tools are named, and why it matters for auditing

Every tool an MCP server exposes shows up in Claude Code under the name mcp__<server>__<tool>: a GitHub server's create_issue becomes mcp__github__create_issue. Servers bundled by a plugin get a longer, scoped name, mcp__plugin_<plugin>_<server>__<tool>. That naming is the whole trick: anything that can filter on a tool name can filter on MCP tools specifically, and the regular expression mcp__.* matches every one of them. Note the .* is required; a matcher written as mcp__github is compared as an exact string and matches nothing.

Option 1: a hook that writes an audit log

Hooks are shell commands (or HTTP endpoints) that Claude Code runs at fixed points in its loop. Three events matter here. PreToolUse fires before a tool call executes and can block it; PostToolUse fires after a call succeeds and receives the result; PostToolUseFailure fires after a call fails. Two more are useful for a stricter trail: PermissionRequest, when a call needs a permission decision, and PermissionDenied, when auto mode refuses one. All of them match on tool name, so a single matcher of mcp__.* scopes the whole thing to MCP. The hooks guide covers the mechanics in general; this is the audit-specific recipe.

Put this in .claude/settings.json for one project, or ~/.claude/settings.json for every project on your machine:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "mcp__.*",
        "hooks": [
          { "type": "command", "command": "$HOME/.claude/hooks/mcp-audit.sh", "async": true }
        ]
      }
    ],
    "PostToolUseFailure": [
      {
        "matcher": "mcp__.*",
        "hooks": [
          { "type": "command", "command": "$HOME/.claude/hooks/mcp-audit.sh", "async": true }
        ]
      }
    ],
    "PermissionDenied": [
      {
        "matcher": "mcp__.*",
        "hooks": [
          { "type": "command", "command": "$HOME/.claude/hooks/mcp-audit.sh", "async": true }
        ]
      }
    ]
  }
}

The hook receives one JSON object on stdin. The fields common to every event are session_id, transcript_path, cwd, permission_mode, hook_event_name, prompt_id and, when the call came from a subagent, agent_id and agent_type. Tool events add tool_name, tool_input and tool_use_id; the post-call events add the result (as tool_response on success, and the error on failure). The script below keeps all of it and appends one line per event to a JSONL file, which is the format every log tool can read:

#!/bin/bash
# ~/.claude/hooks/mcp-audit.sh  (chmod +x)
input=$(cat)
jq -c '{
  ts: (now | todate),
  event: .hook_event_name,
  session: .session_id,
  prompt: .prompt_id,
  agent: .agent_type,
  tool_use_id: .tool_use_id,
  tool: .tool_name,
  input: .tool_input,
  response: .tool_response,
  error: .error,
  cwd: .cwd
}' <<<"$input" >> "$HOME/.claude/mcp-audit.jsonl"
exit 0

A few deliberate choices. The hook exits 0 and never blocks: an audit log that can stop work is a different tool with different failure modes. It is marked "async": true so Claude never waits for it; Claude Code does not enforce a timeout on async command hooks, and a slow disk should not slow the agent. And it keeps tool_use_id, which is the key that joins a PreToolUse record to its PostToolUse, and also joins both to the OpenTelemetry events in option 2.

Reading the log back is a jq one-liner. Every MCP call this week, by server:

jq -r '.tool' ~/.claude/mcp-audit.jsonl | sed -E 's/^mcp__([^_]+(_[^_]+)*)__.*/\1/' | sort | uniq -c | sort -rn

If you want the log to be tamper-resistant, or to capture calls that were blocked rather than merely denied, add the same hook on PreToolUse and, for organisation-wide enforcement, put the hook in managed policy settings with allowManagedHooksOnly set. With that setting, user, project, local and plugin hooks are blocked and disableAllHooks cannot disable the managed ones, so a developer cannot opt out of the audit by editing their own settings. Run /hooks in a session to see which hooks are active and where each one is defined.

Option 2: OpenTelemetry tool events

If your team already ships logs and metrics to Datadog, Grafana, Honeycomb or anything else that speaks OTLP, Claude Code can send tool-call events there directly, with no hook at all. Telemetry is off unless you turn it on, and tool events are log events, so you need a logs exporter as well as the master switch:

export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_LOGS_EXPORTER=otlp          # or console, to see them in the terminal
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_LOG_TOOL_DETAILS=1          # include tool_parameters and full errors

Two events carry the audit trail. claude_code.tool_decision is emitted when a call is accepted or rejected and includes tool_name, tool_use_id, decision (accept or reject), source (config, hook, user_permanent, user_temporary, user_abort or user_reject) and, usefully, tool_source, which is mcp for MCP tools. That last attribute means you can build a dashboard of MCP activity without parsing names. claude_code.tool_result is emitted only after a tool actually runs and carries success, duration_ms, error_type, tool_input_size_bytes and tool_result_size_bytes; with OTEL_LOG_TOOL_DETAILS=1 it also includes the full error message and tool_parameters as a JSON string. Every event has session.id, a prompt.id that groups everything caused by one user prompt, and an event.sequence counter for ordering.

The privacy flags are the part to get right before rolling this out. By default tool parameters are not sent and user prompts are redacted; OTEL_LOG_TOOL_DETAILS opts into parameters, and OTEL_LOG_USER_PROMPTS opts into prompt text. Decide deliberately, because MCP tool parameters routinely contain file paths, issue bodies and query strings. Note too that Claude Code strips the OTEL_* exporter variables from every subprocess it spawns, including hooks, so a hook cannot accidentally inherit and reuse your exporter configuration. The environment variables guide lists the rest of the telemetry switches.

Option 3: the transcript, for the investigation after the fact

Every session is written to a JSONL transcript under ~/.claude/projects/, and the path to the current one is handed to every hook as transcript_path. The transcript contains the full conversation, including every tool use block the model emitted and every tool result that came back, so it is the most complete record of what happened in a session, including MCP calls. It is also the least convenient: it is per session, it is written asynchronously (hooks are warned that it may lag the in-memory conversation), and it is a conversation format rather than a log format. Use it when you need to reconstruct one incident, not as the daily audit. A quick extraction of MCP tool uses from a transcript:

jq -c 'select(.type=="assistant") | .message.content[]? | select(.type=="tool_use" and (.name|startswith("mcp__"))) | {id, name, input}' \
  ~/.claude/projects/<project>/<session>.jsonl

If you name sessions, finding the right file is easier; the post on resuming and naming sessions covers that.

Which one to use

  • Solo developer, one machine: the hook. Five minutes, no infrastructure, one file you can grep.
  • Team with an observability stack: OpenTelemetry, with tool_source = mcp as the filter and OTEL_LOG_TOOL_DETAILS decided by your privacy review. Add the hook anyway if you need a record that survives the exporter being down.
  • Organisation that needs the audit to be mandatory: the hook in managed settings with allowManagedHooksOnly, plus telemetry configured by the same policy.
  • Post-incident: the transcript, joined to the hook log or the telemetry by session_id and tool_use_id.

What a client-side audit cannot tell you

All three records are written by Claude Code, on the client. They tell you what Claude Code asked the server to do and what the server said back. They do not tell you what the server did with the request on the way: a remote MCP server can log, forward or store anything it receives, and a local server running as your user can touch anything you can. The audit trail for the server side lives with the server and its operator, and if that matters to you it is a question for the server's documentation, not a Claude Code setting. The same goes for the model: an audit log records calls, not intentions, so the instructions you give in CLAUDE.md and the permission rules you set are still the first line, with the log as the record of whether they held. If MCP context cost is your other worry, the post on keeping MCP servers from eating your context window is the companion to this one.

One more practical point. A permission prompt for an MCP tool is the moment the audit trail and the human meet: the call is logged, the model is waiting, and if nobody is at the keyboard nothing happens. That wait is exactly what notification tooling exists for.

Keep reading

Desk Exercises for Programmers: 15 Moves to Do While Your AI Agent Codes

7 min read

Exercise While Coding: Turn Claude Code Wait Time Into Micro-Workouts

7 min read

Claude Code Notch Notifications on Mac: See When Claude Is Done Without Watching the Terminal

7 min read

Claude Code Tips and Tricks: 12 Ways to Use Claude Code Effectively

8 min read

Claude Code Multiple Sessions: How to Run Agents in Parallel Without Losing Track

6 min read

A Claude Code Workflow That Doesn't Involve Watching the Terminal

5 min read

Claude Code Hooks: A Practical Guide to Automating Your Agent Workflow

7 min read

Claude Code Auto Mode: Fewer Permission Prompts Without Living Dangerously

7 min read

Claude Code Subagents: How to Delegate Work to Specialized Agents

7 min read

Claude Code Context Management: Treat the Context Window Like a Budget

6 min read

Per-Subagent Model Selection: Route the Grunt Work Down, Keep the Judgment Up Top

7 min read

Skills and Plugins: How to Teach Claude Code Your Way of Working

7 min read

Claude Code Background Tasks: Run Long Commands Without Blocking Your Session

6 min read

Codex CLI Notifications: How to Get a Ding When Codex Is Done or Needs Input

7 min read

Cursor Notification When Done: Every Way to Get Notified When Cursor Finishes

6 min read

Claude Code Notifications: How to Get Notified When Claude Code Finishes or Needs Your Input

6 min read

Want to Be Notified When Claude Responds? How Claude Notifications Work on Web, Desktop, and Mobile

5 min read

Claude Code Notification Scripts: Copy-Paste Recipes for Every Platform

6 min read

Get a Ding the Moment Codex Needs Your Response

6 min read

Get Notified the Moment Claude Code Is Waiting for Your Input

6 min read

“Notifications Are Turned Off for Claude” — Here Is the Fix

6 min read

Codex Sound When Done: Make Codex CLI Play a Sound When It Finishes

6 min read

terminal-notifier + Claude Code: Native macOS Alerts When Your Agent Finishes

6 min read

Gemini CLI Notifications: How to Get a Sound or Alert When Gemini Finishes

6 min read

Get Claude Code Notifications on Your Phone

6 min read

Claude Code Remote Control, Explained

7 min read

preferredNotifChannel: Claude Code's Built-In Notification Setting

6 min read

Claude Code Notifications in tmux and Over SSH

7 min read

Claude Code Effort Levels: Why Your Setting Keeps Getting Ignored

8 min read

Codex vs Claude Code Notifications: How Each One Tells You It Is Done

8 min read

Claude Code Notifications Not Working: A Diagnostic Checklist

10 min read

Claude Code Notifications Inside Your Editor's Terminal

7 min read

CLAUDE_CODE_DISABLE_BACKGROUND_TASKS Explained

7 min read

CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP Explained

8 min read

Cursor Alerts Explained: Every Alert Cursor Raises, and How to Control Them

7 min read

Claude Code Environment Variables: The Groups That Actually Matter

10 min read

Claude Code Notifications on WSL and Windows

9 min read

Claude Code Checkpoints and /rewind

8 min read

Claude Remote Control: How It Works, Turning It Off, and Auto Mode

7 min read

Claude Code /loop: Poll a Deploy, Babysit a PR, and Get Told When It's Done

8 min read

Setting Up NotchFit With OpenAI Codex CLI: Hooks, $notchfit-plan, and the Daily Flow

7 min read

Claude Mods Explained: What the New Plugin Layer Can Do, and How to Turn On 'You Should Know'

9 min read

NotchFit vs Mac Break Reminder Apps: A Clock or a Signal From Your Coding Agent?

8 min read

Claude Code Headless Mode: Running claude -p in Scripts and CI, and the Three Signals That It Finished

10 min read

Building a Movement Habit at a Desk: Use the Agent Run as the Cue, One Set as the Unit, and Streaks Carefully

8 min read

Claude Code MCP Servers Explained: Adding Servers, Choosing a Scope, Handling Secrets, and Why Long Tool Calls Go to the Background

10 min read

/notchfit:plan Explained: How Claude Code or Codex Writes a Weekly Workout Plan the Notch Can Actually Run

8 min read

Claude Code Spinner Verbs Explained: What “Pondering” and “Brewing” Mean, How to Change Them, and Why You Should Stop Watching the Spinner

7 min read

Why NotchFit's Done and Permission Alerts Wait Until Your Set Ends, When That Costs You, and How to Turn the Workout Off

7 min read

How to Resume a Claude Code Session: --continue vs --resume, Naming Sessions, the Session Picker, Branching, and What Comes Back

9 min read

NotchFit on a Mac Without a Notch: How Pill Mode Works on Mac mini, iMac, Mac Studio and a MacBook With the Lid Closed

6 min read

How to Run a Claude Code Session in the Background: /background, claude --bg, Agent View, and Getting Back to It

9 min read

What a Notch App Can See: NotchFit's Local-First Design, What the Claude Code and Codex Hooks Send, and What Stays on Your Mac

7 min read

Why Codex CLI Asks for Permission Every Time, and How to Tune the Sandbox and Approval Policy Without Going Full Access

8 min read

What to Do While Claude Code or Codex Works: A Simple Protocol for the Two-to-Twenty-Minute Gap in Every Agent Run

7 min read

Is It Possible to Get a Notification When Claude Code or Codex Hits a Reset? Yes for Claude Code: the quota_auto_resume Hooks, Explained

8 min read

What a MacBook Notch App Can Actually Do: The Five Kinds of Notch Utility, Their Trade-offs, and Where NotchFit Fits

7 min read

How to Make MCP Servers Not Take Up as Much Context in Claude Code: Tool Search, alwaysLoad, Scopes and Output Caps

8 min read

The notchfit Command Line: Drive Your Workout, Read Status as JSON, Validate Plans and Test the Notch Without Leaving the Terminal

7 min read

Exercise Snacks: What the Research Actually Says About One-Minute Vigorous Breaks, What It Does Not, and How NotchFit's Agent-Run Sets Compare

8 min read