Follow these steps to configure Claude Code to send hook events to the dd-apm-test-agent. This enables LLM Observability traces for your Claude Code sessions — you can view tool calls, subagent spans, and LLM invocations.
- Docker installed and running
curlavailable on the system
Before starting the test agent, check if one is already running:
curl -s http://localhost:8126/infoIf this returns a JSON response where the version field contains "test" (e.g. "version": "test"), the test agent is already running. Skip ahead to Step 2 — no need to start another one.
Note: The regular Datadog Agent also listens on port 8126 and has an /info endpoint, but it returns a numeric version like "7.45.0". Checking for "test" in the version field reliably distinguishes the test agent from the Datadog Agent.
If curl fails, returns nothing, or the version does not contain "test", check whether another process is using port 8126:
lsof -i :8126 -sTCP:LISTENIf the command produces no output, the port is free — proceed to the docker run command below.
If a process is listed, note its name and PID from the output. Inform the user:
Port 8126 is currently in use by
<process name>(PID<pid>). The test agent needs this port to receive traces and hook events.Note: If the conflicting process is the Datadog Agent, stopping it will prevent APM traces, metrics, and other telemetry from being submitted to datadoghq.com while it is stopped.
Do you want to stop
<process name>so the test agent can start?
Wait for explicit user confirmation before proceeding. Do not stop the process without a "Yes."
After the user confirms, stop the process and verify the port is free:
kill <pid>Then re-check:
lsof -i :8126 -sTCP:LISTENIf the process is still listening after a few seconds, inform the user and ask whether to force-kill it:
kill -9 <pid>Once the port is free, proceed to start the test agent.
Ask the user if they would like to forward traces to Datadog to persist them and get additional features like cost estimation. If they do, ask them for their DD_API_KEY and restart the test agent with the key:
docker run --rm -p 8126:8126 \
-e HOST_USER="$USER" \
-e DD_API_KEY=<their-api-key> \
-e DD_SITE=datadoghq.com \
ghcr.io/datadog/dd-apm-test-agent/ddapm-test-agent:latestThe HOST_USER env var passes your username into the container so spans are tagged with user_name:<you> instead of root.
If they don't have an API key, they can create one at https://app.datadoghq.com/organization-settings/api-keys. If they prefer local-only mode, skip this step, and instead run the test agent with:
docker run --rm --pull always -p 8126:8126 -e HOST_USER="$USER" ghcr.io/datadog/dd-apm-test-agent/ddapm-test-agent:latestVerify it's running:
curl -s http://localhost:8126/info | head -c 100Add the following to ~/.claude/settings.json. If the file already exists, merge the hooks key into the existing config.
{
"hooks": {
"PreToolUse": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "curl -s --max-time 2 -X POST -H 'Content-Type: application/json' -d @- http://localhost:8126/claude/hooks >/dev/null 2>&1 || true",
"async": true
}
]
}
],
"PostToolUse": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "curl -s --max-time 2 -X POST -H 'Content-Type: application/json' -d @- http://localhost:8126/claude/hooks >/dev/null 2>&1 || true",
"async": true
}
]
}
],
"PostToolUseFailure": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "curl -s --max-time 2 -X POST -H 'Content-Type: application/json' -d @- http://localhost:8126/claude/hooks >/dev/null 2>&1 || true",
"async": true
}
]
}
],
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "curl -s --max-time 2 -X POST -H 'Content-Type: application/json' -d @- http://localhost:8126/claude/hooks >/dev/null 2>&1 || true",
"async": true
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "curl -s --max-time 2 -X POST -H 'Content-Type: application/json' -d @- http://localhost:8126/claude/hooks >/dev/null 2>&1 || true",
"async": true
}
]
}
],
"SubagentStart": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "curl -s --max-time 2 -X POST -H 'Content-Type: application/json' -d @- http://localhost:8126/claude/hooks >/dev/null 2>&1 || true",
"async": true
}
]
}
],
"SubagentStop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "curl -s --max-time 2 -X POST -H 'Content-Type: application/json' -d @- http://localhost:8126/claude/hooks >/dev/null 2>&1 || true",
"async": true
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "curl -s --max-time 2 -X POST -H 'Content-Type: application/json' -d @- http://localhost:8126/claude/hooks >/dev/null 2>&1 || true",
"async": true
}
]
}
],
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "curl -s --max-time 2 -X POST -H 'Content-Type: application/json' -d @- http://localhost:8126/claude/hooks >/dev/null 2>&1 || true",
"async": true
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "curl -s --max-time 2 -X POST -H 'Content-Type: application/json' -d @- http://localhost:8126/claude/hooks >/dev/null 2>&1 || true",
"async": true
}
]
}
],
"PreCompact": [
{
"hooks": [
{
"type": "command",
"command": "curl -s --max-time 2 -X POST -H 'Content-Type: application/json' -d @- http://localhost:8126/claude/hooks >/dev/null 2>&1 || true",
"async": true
}
]
}
],
"PermissionRequest": [
{
"hooks": [
{
"type": "command",
"command": "curl -s --max-time 2 -X POST -H 'Content-Type: application/json' -d @- http://localhost:8126/claude/hooks >/dev/null 2>&1 || true",
"async": true
}
]
}
]
}
}hooks: Each hook fires a curl command that POSTs the hook event JSON (read from stdin via-d @-) to the test agent. The--max-time 2timeout and|| trueensure hooks never block Claude Code, even if the agent is down.
The hooks alone capture tool and agent spans. To also capture LLM spans (token counts, model info, input/output messages, and span links), set the ANTHROPIC_BASE_URL environment variable when launching Claude Code (see Step 5).
Direct the user to open the local dev experience in their browser to view traces:
https://app-30bd13e67e6cba3b6c36f48da9908a7a.datadoghq.com/llm/traces?devLocal=true&enable-rum
This connects to the local test agent and displays traces as they arrive. The enable-rum query parameter enables RUM data collection on hash links (it's disabled by default). This only needs to be added once — it persists in localStorage for subsequent visits.
Start a new Claude Code session with the fetch interceptor (preferred):
lapdog-run claudeThis injects a Javascript module via BUN_OPTIONS that patches fetch() to route Anthropic API calls through the test agent gateway. It works even when managed settings override ANTHROPIC_BASE_URL, and automatically applies to subagent processes (since BUN_OPTIONS is inherited by child processes).
Fallback method — if lapdog-run is not available, use the environment variable directly:
ANTHROPIC_BASE_URL=http://localhost:8126/claude/proxy claudeBoth methods route API calls through the test agent's proxy for full LLM span capture. Each user turn produces a trace with:
- A root agent span for the session turn
- LLM spans for each Anthropic API call (with token metrics)
- Tool spans for each tool invocation
- Subagent spans for Task tool delegations
- Span links connecting LLM outputs to tool inputs and vice versa
Without the proxy, hooks still capture tool and agent spans, but LLM spans and span links won't be included. When the test agent is stopped, Claude Code still works — the interceptor's requests fall back to the original URL, and the hooks fail silently.
Replace http://localhost:8126 in the hook curl commands in settings.json with your test agent's URL. For the proxy, either set DDAPM_GATEWAY_URL when using lapdog-run, or replace the URL in ANTHROPIC_BASE_URL when using the fallback method.
If you only want hook-based tracing without LLM span capture, launch claude directly (without lapdog-run or ANTHROPIC_BASE_URL). You will still get tool and agent spans but not LLM spans or span links.
GET http://localhost:8126/claude/hooks/sessions— list tracked sessionsGET http://localhost:8126/claude/hooks/spans— return all assembled spansGET http://localhost:8126/claude/hooks/raw— return all raw received hook events
Hooks not sending events: Verify the agent is running with curl http://localhost:8126/info. Check that ~/.claude/settings.json is valid JSON. Start a new Claude Code session after changing settings.
No LLM spans: Ensure you launched Claude with lapdog-run claude (or with ANTHROPIC_BASE_URL=http://localhost:8126/claude/proxy claude). The proxy must be reachable for LLM span capture to work.
Missing spans: Some events (like Stop) may arrive after the session ends. Check GET /claude/hooks/raw to see all received events.