---
title: 'Claude Code safety_event: What It Logs, Tested'
description: Claude Code 2.1.307 added a safety_event OpenTelemetry event. I ran 2.1.306, 2.1.307 and 2.1.310 to see which refusals it logs and which it skips in -p runs.
url: https://nerdleveltech.com/claude-code-safety-event-opentelemetry-tested
language: en
date: '2026-10-26'
category: security
tags:
  - claude code
  - OpenTelemetry
  - claude code safety_event
  - claude code permissions
  - ai agent audit logging
  - agentic coding
  - developer tools
markdown_url: https://nerdleveltech.com/claude-code-safety-event-opentelemetry-tested.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/claude-code-safety-event-opentelemetry-tested'
terms: https://nerdleveltech.com/llms.txt
---

# Claude Code safety_event: What It Logs, Tested

`claude_code.safety_event` is an OpenTelemetry event that Claude Code 2.1.307 added to log tool calls that a permission rule, auto mode, a safety check, a hook or the sandbox stopped, prompted or sent back. In my `claude -p` runs on 2.1.310 it fired for deny rules, ask rules, `dontAsk` mode, hooks and two built-in safety checks, and it did not fire when a `-p` run refused a Bash command that nothing granted.

I kept the logs of 108 headless sessions run with the console log exporter, to see what the event contains and what it leaves out. I found a gap worth knowing about before you build an alert on it.

## TL;DR

- `claude_code.safety_event` first appeared in 2.1.307. On 2.1.306, the three refusal cases I ran on every build produced only a `tool_decision` event. On 2.1.307 and 2.1.310 they produced both.
- It covered deny rules from five sources (project, local, a `--settings` file, the `--disallowedTools` flag, managed settings), an ask rule in project settings, `dontAsk` mode, `--permission-prompts none`, a `PreToolUse` hook deny, a `PostToolUse` block, a Read outside the working directory with no allow rule, and a Write to `.claude/settings.json`.
- It did not fire when a `-p` run refused a Bash command that nothing granted. That refusal appeared only as a `tool_decision` event with `decision` set to `reject`. The docs describe this case as a prompt with no category. A denied tool that the model never called, `WebFetch`, produced no event for that tool.
- The event's own attributes held no Bash command text, including in the runs with `OTEL_LOG_TOOL_DETAILS=1`. The command appeared on the matching `tool_decision` event. All 42 safety events in my 69 main runs on 2.1.310 shared a `tool_use_id` with a `tool_decision` event.
- I tested `claude -p` only, with the `haiku` alias. Auto mode, the sandbox categories, interactive prompts and OTLP export are untested.

## What You'll Learn

- What `claude_code.safety_event` is and which version added it
- How to print it with the console exporter using two environment variables
- Which refusals produce an event, with the category and detail attribute for each
- Which refusals do not, and how to find them
- What the event leaves out, and how to join it to the command

## What the event is

`claude_code.safety_event` is a structured log event that Claude Code emits when something stops, prompts for, or sends back a tool call. The [changelog](https://code.claude.com/docs/en/changelog) lists it under 2.1.307, dated October 21, 2026: "Added the `claude_code.safety_event` OpenTelemetry event for each tool call a permission rule, auto mode, a safety check, a hook or the sandbox stopped, prompted or sent back".[^1]

The [monitoring documentation](https://code.claude.com/docs/en/monitoring-usage) says the event "exports with the other events and has no separate setting."[^2] If you already ship Claude Code telemetry, it arrives with the rest. The docs define 14 values for the `category` attribute: `permission_rule`, `managed_policy`, `permission_mode`, `hook`, `auto_mode_block`, `auto_mode_no_verdict`, `safety_check`, `static_analysis`, `sandbox_override`, `user_rejected`, `sandbox_network`, `sandbox_other`, `hook_review` and `other`.[^2]

The `decision` attribute is `accept`, `reject`, or `sent_back` when a hook blocked a result or the end of a turn.[^2]

## How I tested it

The setup is two required environment variables and a shell. I also shortened the export interval to one second. I did not test whether the default would have flushed before a short run exited.

```bash
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_LOGS_EXPORTER=console
export OTEL_LOGS_EXPORT_INTERVAL=1000   # docs default is 5000

claude -p "Use the Bash tool to run exactly this command and nothing else: touch made-by-lab.txt
If the tool is refused, reply with the single word REFUSED and stop." \
  --model haiku --max-turns 4 --disallowedTools "Bash(touch:*)" < /dev/null > run.txt

grep -A 30 'body: "claude_code.safety_event"' run.txt
```

The console exporter writes each event to stdout as a JavaScript style object, not JSON, so the model's reply and the events share one stream. I used `grep` and a small parser to pull out the attributes. The `< /dev/null` in the command only skips a stdin warning, and my scripted runs did not use it.

Every run used its own fresh directory, the `haiku` alias and Linux x86_64, on October 26, 2026. The logs show the model as `claude-haiku-5-5` in every reported run, on all three builds. 2.1.310 was the `latest` tag on npm when I checked.[^3] I used `touch made-by-lab.txt` as the command. Three runs with `echo` in `dontAsk` mode were allowed with no rule at all, so `echo` could not show a refusal, and I dropped it.

I ran 23 configurations three times each on 2.1.310, which is 69 runs. For the version comparison I installed 2.1.306 and 2.1.307 from npm in their own folders and ran three cases three times each on both, plus the same three cases again on 2.1.310. That is 27 runs. Four follow-up checks of three runs each, the `echo` check above and three Read and Write checks described below, bring the total to 108 sessions with saved logs.

I had run the 2.1.306 and 2.1.307 cases once earlier, three runs per case, without keeping the logs. The results matched, and the table uses the rerun.

Two of the 23 configurations are not reported as results. One was a `PostToolUse` test whose allow rule sat in project settings, which a quirk described below confounded, so I redid it with the allow rule on the command line. The other was a plan mode write test where the model declined before calling the tool in two of three runs, and whose logs show a second model, `claude-sonnet-5-5`, so I left it out. In the results table and the version table, each case gave the same event types and the same attribute values in all three runs. I left event ids and sequence numbers out of that comparison.

## Results on 2.1.310

![Table of the safety_event category and detail attribute reported for each refusal case on Claude Code 2.1.310, with two rows where no safety_event appeared. One of them is a refused Bash command that nothing granted.](https://nerdleveltech.com/images/claude-code-safety-event-opentelemetry-tested-results.png)

Rule denials report which settings layer supplied the rule. The `rule_source` value matched where I put the rule each time: `projectSettings`, `localSettings`, `flagSettings` for a `--settings` file, `cliArg` for `--disallowedTools`, and `policySettings` for a managed settings file I placed at `/etc/claude-code/managed-settings.json`. The managed case also changes the category to `managed_policy`, so a managed rule is easy to separate from a developer's own deny rule.

An ask rule in a `-p` run was refused in my test, which fits a session where nobody can answer a prompt. It was logged as `permission_rule` with `rule_source` set to `projectSettings`. A deny rule in project settings also beat an allow for the same command on the command line (`--allowedTools`), three runs out of three, and was logged the same way.

Hooks split in two. A `PreToolUse` hook that returned a deny decision gave category `hook`, `decision` `reject`, and `reason_code` `hook:PreToolUse`. A `PostToolUse` hook that returned a block gave category `hook_review` and `decision` `sent_back`. In that second case the matching `tool_decision` event said `accept`, because the allow rule had approved the call before it ran. The hook blocked the result afterward.

I tested two built-in checks, and both landed under `safety_check`. A Write to `.claude/settings.json` in `acceptEdits` mode carried `circuit_breaker` set to `claudeSettingsFile`. It stayed refused, with the same event, when I added `--allowedTools Write`, three runs out of three. A Read of `/etc/hostname`, which is outside the working directory, carried `reason_code` set to `workingDir` and no `circuit_breaker` attribute. That Read had no allow rule. With `--allowedTools Read` the same Read was accepted and logged no safety event, three runs out of three. So in my test an allow rule granted the outside read, while the settings file write stayed refused even with an allow. I tried one path and one rule form for each. My first Read test used an allow rule in project settings, which a quirk described below ignored. I reran it with no allow rule at all and got the same event. The docs list `circuit_breaker` as the detail attribute for this category, with `outsideReadsBlocked` as one example value.[^2] I did not see that value on my plain Read. Treat the exact attribute for an outside read as something to confirm on your own build.

## It first appeared in 2.1.307

I ran a deny rule in project settings, `dontAsk` mode, and a `PreToolUse` hook deny on 2.1.306, 2.1.307 and 2.1.310, three runs each, with the same prompt and settings on every build.

| Case | 2.1.306 | 2.1.307 | 2.1.310 |
| - | - | - | - |
| Deny rule in project settings | `tool_decision` only | `tool_decision` + `safety_event` | `tool_decision` + `safety_event` |
| `dontAsk` mode | `tool_decision` only | `tool_decision` + `safety_event` | `tool_decision` + `safety_event` |
| `PreToolUse` hook deny | `tool_decision` only | `tool_decision` + `safety_event` | `tool_decision` + `safety_event` |

npm lists 2.1.306 as published on October 21, 2026 at 02:28 UTC and 2.1.307 at 20:47 UTC the same day, which matches the changelog.[^3] 2.1.306 is the only older build I ran, but the changelog lists the event under 2.1.307, so a dashboard built on it should show nothing for machines on older builds.

## The gap: refusals with no event

The docs say some refusals produce no safety event. One is a `-p` run where a prompt had no category and nobody could answer it, "with neither `--permission-prompt-tool` nor `--permission-prompts none`".[^2]

I saw the same behavior. With no rule granting `touch`, the `-p` run refused the call, the `tool_decision` event said `reject` with `source` `config`, and no safety event followed, three runs out of three. Adding `--permission-prompts none` to the same command produced a safety event with category `permission_mode`, `reason_type` `asyncAgent` and `reason_code` `unknown`.

Our read: an alert built only on `safety_event` will miss one headless failure that unattended jobs can run into, which is an agent asking for something nobody pre-approved. The docs' own suggestion is to look for `tool_decision` events that report `reject` with `source` `config` and no safety event sharing their `tool_use_id`.[^2] If you run unattended jobs, build that second query as well, or pass `--permission-prompts none` so those refusals get a category.

A denied tool that the model never called is not a refusal, and it left no event either. With `WebFetch` denied, the model said in all three runs that the tool was not available, and I saw no `safety_event` in any of them. In every run the model first searched for the tool with `ToolSearch`. In two of the three runs its replies say it then tried `curl` through Bash, and those refusals had no safety event either, which is the gap above. I did not run a control without the deny rule. The `WebFetch` result is consistent with my [MCP permission rules test](https://nerdleveltech.com/claude-code-mcp-allow-deny-rules-server-names-tested), where deny rules took a tool out of the session's tool list in the runs I inspected, although the model still tried to call it in 8 of the 11 runs where a tool was missing. I did not inspect the tool list here, so I am inferring it from what the model said.

## What the event holds, and how to join it

The docs say the event's own attributes hold no command text, file paths, host names, or MCP server and tool names.[^2] The attribute lists in my Bash, Read and Write runs held none of those. No MCP tool was called in any run, so I did not check MCP server or tool names. With `OTEL_LOG_TOOL_DETAILS=1` set on one deny case, three runs, the `tool_decision` event carried the full command in `tool_parameters`, and the `safety_event` for the same call did not.

The two events join on `tool_use_id`. Across the 69 runs on 2.1.310, all 42 safety events matched a `tool_decision` event with the same `tool_use_id`. In 39 of them the safety event came one `event.sequence` number after the decision. In the other three, the `PostToolUse` cases, it came five later, after the tool had run.

The record also carries the standard attributes, which the docs say can include `workspace.host_paths` and the repository attributes.[^2] In my console output they also included `user.email` and account identifiers. If you forward these events to a shared collector, check what the standard attributes expose before you do.

## One thing I saw and did not explain

An allow rule written to `.claude/settings.json` in my scratch directory did not take effect in `-p` runs. The `touch` call was refused with no safety event, three runs out of three, and again in a second directory with a stub `.git` folder. The same allow rule in `settings.local.json`, in a `--settings` file or in `--allowedTools` worked. A deny rule in `.claude/settings.json` applied normally. I did not chase the cause, and I would not generalize beyond my setup, but it explains why an early version of my test showed refusals I had expected to be allowed.

## What I did not test

- Interactive sessions, where a person answers prompts. The docs list `user_rejected` for a declined or dismissed prompt, which I did not trigger.
- Auto mode: `auto_mode_block`, `auto_mode_no_verdict` and the `classifier_category` attribute.
- The sandbox categories `sandbox_network`, `sandbox_other` and `sandbox_override`, and `static_analysis`.
- OTLP export. I used the console exporter only.
- MCP tools, subagents, skills and plugins.
- Plan mode, and `PermissionRequest` and `Stop` hooks. The docs mention them under `permission_mode`, `hook` and `hook_review`, and my one plan mode test was inconclusive.
- User level settings as a rule source, other models, other operating systems and Bedrock, Vertex or Foundry.
- Builds other than 2.1.306, 2.1.307 and 2.1.310.

## Related reads

- [Claude Code MCP Permission Rules: 2.1.299 Tested](https://nerdleveltech.com/claude-code-mcp-allow-deny-rules-server-names-tested)
- [Claude Code Hook Failures: onFailure Block Tested](https://nerdleveltech.com/claude-code-hook-fail-closed-onfailure-tested)
- [Claude Code updatedInput Hooks: 2.1.300 Change Tested](https://nerdleveltech.com/claude-code-pretooluse-updatedinput-validation-tested)
- [Claude Code Mods: 10 Tested Use Cases (2026)](https://nerdleveltech.com/claude-code-mods-use-cases-examples)
- [AI Security Fundamentals: Attacks, Defenses & Guardrails](https://nerdleveltech.com/courses/ai-security-fundamentals), the course on guardrails and prompt injection defense

## FAQ

### What is claude_code.safety_event?

It is an OpenTelemetry log event added in Claude Code 2.1.307 that records tool calls that a permission rule, auto mode, a safety check, a hook or the sandbox stopped, prompted or sent back.[^1] Its `category` attribute says which check acted, and `decision` says whether the call was accepted, rejected or sent back.[^2]

### How do I see safety events locally?

Set `CLAUDE_CODE_ENABLE_TELEMETRY=1` and `OTEL_LOGS_EXPORTER=console`, then run `claude -p` and read stdout. The event has no separate setting.[^2] In my `-p` runs the events printed to standard output, mixed with the model's reply.

### Does a safety event include the command that was blocked?

No. The docs say the event's own attributes hold no command text, file paths or server names, and the attribute lists in my Bash, Read and Write runs held none of those.[^2] In my three runs with `OTEL_LOG_TOOL_DETAILS=1`, the command appeared only on the `tool_decision` event, which you join by `tool_use_id`.

### Why did my blocked command have no safety event?

In a `-p` run, a prompt with no category that nobody can answer is refused without a safety event, and the docs list this case.[^2] I saw the same behavior with a Bash command that no rule granted, in 3 of 3 runs. The refusal still shows up as a `tool_decision` event with `decision` `reject` and `source` `config`. Passing `--permission-prompts none` gave the same call a `permission_mode` safety event.

### Which Claude Code version added safety_event?

Version 2.1.307, published October 21, 2026.[^1][^3] I confirmed it by running three builds: 2.1.306 emitted only `tool_decision` for three refusal cases, and 2.1.307 and 2.1.310 emitted both events.

## Bottom Line

In my `claude -p` runs, `claude_code.safety_event` gave a usable audit trail for rule denials, hooks and two built-in checks, with the settings layer named for each rule. It stayed silent when a headless run refused a Bash command that nothing granted, so pair it with a query on `tool_decision` rejects that have no matching safety event.

Start with the console exporter on one machine, trigger a deny rule, and read the record. Then decide whether the standard attributes are safe to ship before you point the exporter at a shared collector.

## References

[^1]: [Claude Code changelog](https://code.claude.com/docs/en/changelog), Anthropic, version 2.1.307, October 21, 2026, read October 26, 2026. Source for the release of `claude_code.safety_event` and its quoted description.
[^2]: [Monitoring](https://code.claude.com/docs/en/monitoring-usage), Claude Code documentation, Anthropic, read October 26, 2026. Source for the event description, the 14 category values, the attribute list, the export behavior, the `OTEL_LOG_TOOL_DETAILS` flag and the cases without a safety event.
[^3]: [@anthropic-ai/claude-code on npm](https://www.npmjs.com/package/@anthropic-ai/claude-code), publish times and dist-tags queried with `npm view`, October 26, 2026. Source for the 2.1.306 and 2.1.307 publish times and for 2.1.310 as the `latest` tag.
