Pulse

What Pulse collects

The same policy pulse what-is-collected prints on the command line. This page is GENERATED from the two allowlist tables in internal/redact/redact.go — the same tables the agent filters with — so it cannot drift from the behaviour. A test fails the build when it does.

This page needs no account on purpose. It is what you should read before installing the agent, not after.

What is never collected, on any machine, ever

  • Prompt text, in whole or in part
  • Assistant responses, in whole or in part
  • Tool inputs, including file contents passed to a tool
  • Tool results and their contents
  • Shell commands and their arguments
  • Raw file paths, repository paths and directory names
  • Error messages and response bodies from a vendor API
  • Device serial numbers, which are hashed before they ever leave the machine
  • Credentials, tokens and vendor API keys

There is deliberately no attribute field for any of these in internal/wire. The absence is structural, not a policy that someone has to remember to apply.

Why an allowlist, and why the hooks are the reason

This is the finding from the M0 verification that shaped the whole privacy model.

Claude Code redacts prompt and response content over OTLP, and it is reasonable to assume that is the whole story. It is only half the story. The hook payloads are not redacted: they carry the raw prompt on UserPromptSubmit and the full assistant reply on Stop, unaffected by the OTEL_LOG_* settings. During verification a single Stop hook delivered 1350 characters of assistant response from a real work session.

Hooks ignore the OpenTelemetry flags entirely, so the privacy promise cannot rest on CLI configuration. It rests on the agent projecting hook input down to an explicit allowlist before anything is sent. That is why the list below is an allowlist rather than a denylist: a denylist fails open, so any new content-bearing attribute a future CLI release adds would be forwarded until somebody noticed. An allowlist fails closed, which for privacy is the only defensible direction.

These are the payload fields the hook delivers and the agent drops. They are named here from hook.ContentFields(), the same list the agent's own tests assert against:

promptlast_assistant_messagetool_inputtool_responsetool_input.commandtool_response.stdouttranscript

The policy is also enforced over OTLP, not just over hooks, because the same reasoning applies there: an unrecognized attribute is dropped at the machine.

Pulse reads your Claude Code login token from the Keychain

The one behaviour on this page that is not obvious from the outside, so it is spelled out.

Pulse reads the Claude Code login token from your Keychain to poll Anthropic's usage endpoint for limit windows; the token never leaves your Mac.

What that buys is the only accurate answer to "how much of the shared quota is left". The alternative sources are worse: the status line reports a figure only while a session is running, and a Codex session log reports one only while Codex is open — so on a quiet afternoon the dashboard would show a number from yesterday and present it as current.

What leaves the machine is the RESULT of that poll: a percentage, a window kind, a reset time and the name of the source. The request is made from your Mac, to Anthropic, with your own credential, exactly as the CLI itself does.

macOS will ask for Keychain access the first time, and refusing is a supported choice. Pulse then falls back to the status line and the session logs, marks the source as degraded on the Machines page, and everything else keeps working. It does not retry in a loop and it does not ask again on every poll.

13
namespaces allowed by prefix
A key is kept when it starts with one of these.
91
keys allowed by exact match
Matched in full. Never by prefix, so a new sibling key cannot ride in.
2048
byte cap on any single value
A longer string value is dropped, not truncated: a shortened document is still a document.

Allowed by prefix

Everything not matching one of these, or one of the exact keys below, is dropped at the machine and counted. The prose in each group is the Go source's own comment.

Identity and account. Shared accounts mean these say which account, never which person.

3
user.organization.identity.

Session and event structure.

2
session.event.

Model and usage, as the metric attribute namespaces.

2
token.cost.

Repository identity. These are the CLI's own normalized values: no raw path ever appears here, which is what makes them safe.

1
vcs.

Host and runtime facts added by the agent or the CLI.

5
host.os.service.terminal.pulse.

Allowed by exact key only

These are matched in full. message.uuid is the worked example: allowing a message. prefix would let message.content through.

1
message.uuid

Session and request structure. These used to live in allowedPrefixes as single tokens, which silently admitted every key that started with them.

5
prompt.idclient_request_idrequest_idtool_use_idtool_name

Model and usage.

22
modeltypequery_sourcestart_typeeffortspeedservice_tierinput_tokensoutput_tokenscache_read_tokenscache_creation_tokenscost_usdcost_usd_microsduration_msttft_msresponse_lengthprompt_lengthsuccessdecisiondecision_typedecision_sourceerror_type

Claude Code hook telemetry, which is structural rather than content.

38
hook_eventhook_namehook_sourcehook_typenum_hooksnum_successnum_blockingnum_cancellednum_non_blocking_errortotal_duration_msmanaged_onlysafe_modenum_sessionsagent.nameskill.nameplugin.nameplugin.scopeplugin.versionplugin_id_hashmarketplace.namemcp_server.namemcp_tool.namemcp_server_scopeserver_scopetransport_typestatusis_pluginhas_hookshas_mcphost_owned_mcpenabled_viaagent_path_countcommand_path_countskill_path_countadditionalContextLimitworkflow.run_idtool_input_size_bytestool_result_size_bytes

Codex OpenTelemetry attributes, taken from a real export captured by pointing a scratch CODEX_HOME at a local receiver. These are named explicitly rather than by prefix, because Codex has no namespacing convention for its own attributes. conversation.id is the critical one. It is Codex's ONLY session join key: Codex emits no session.id at all, so dropping it makes every Codex record unattributable and the whole signal useless. That is exactly what an earlier version of this allowlist did.

10
conversation.idslugapp.versionoriginatorprovider_namereasoning_summaryapproval_policymcp_serversstartup.phasestartup.status

"env" is Codex's DEPLOYMENT TAG, the counterpart of environment = "prod" in its otel config. It is not the process environment: no environment variable is ever read or forwarded. The name is confusing enough that it is called out here, and TestCodexOtelKeysAreHandled asserts an actual environment-variable key is still dropped.

4
envtelemetry.sdk.languagetelemetry.sdk.nametelemetry.sdk.version

HTTP detail. The status code is what identifies a 429, which is how a rate limit is detected. `endpoint` and `error.message` used to be here. They are not any more: an error body quotes the request that failed, and the URL of a request is not needed to count one. See the note on MaxValueBytes above.

11
http.response.status_codeattemptauth.modeauth.outcomeauth.stepauth.recovery_reasonauth.request_idauth.retry_after_unauthorizedauth.header_attachedauth.connection_reusedauth.cf_ray

Deliberately absent

Read from the comment above allowedPrefixes in internal/redact/redact.go.

There is no allowlisted prefix for any of these, and the omission is intentional:

prompt.response.message.content.body.command.path.error.

Note that a generic error prefix is absent too. Pulse records error_type, which is a category, rather than an error message, which would be content.

How a value is filtered

The mechanics, so the promise is checkable rather than asserted.

  • Fails closed. A key that does not match the allowlist is dropped and counted. Dropped keys are reported as counts and as first dot-delimited segments, so a vendor rename shows up as unrecognized attributes rather than as silently missing data.
  • Dropped values are never logged. Recording what was stripped would defeat the point, so only key names and counts leave the machine.
  • Strings are capped, not cut. A string longer than 2048 bytes is dropped rather than truncated, because a truncated document is still a document.
  • Nested key-value lists are dropped. They are not recursed into, because an attribute named metadata could otherwise carry arbitrary sub-keys whose names the policy never sees.
  • Arrays are filtered element by element. A nested blob cannot smuggle content past the key check.
  • The server can extend the list but never shorten it. A server push can allow an extra namespace without an agent release. It cannot switch content collection back on, so a compromised or mistaken server push cannot turn the fleet into a content collector.

Retention

How long each kind of data lives, and what the server deletes on its own.

DataKept forWhat it is
Raw events90 daysPer-request and per-event rows: session events, forwarded OTLP batches, tool-call names, and the individual limit readings behind the quota charts.
Presence7 daysWhich machine had a session open and when. Only ever a device and an account, never a person — the person label is something an admin types.
AggregatesKeptToken counts, cost totals and per-session outcomes. These carry no content at all and are what makes a year-on-year comparison possible, so they are not deleted.
Enrolment codesUntil used or expiredOnly a hash is ever stored, and the row is deleted once the code is spent or its time is up.

Deletion is a job on the server, running hourly, not a promise on a page. Raw rows past 90 days and presence rows past 7 days are removed whether or not anyone asks.

Pausing a machine removes the Pulse configuration blocks from the CLI rather than setting a flag, so a paused machine genuinely reports nothing. The Machines page shows which devices are paused.