---
title: "Activity record"
---

# Activity record

What the CLI's activity capture records about a session, when it is on, what it never sends, how events are delivered, and how to inspect them.

Activity capture records what the agent did in a session, such as model calls, tool calls, edits and approvals, and sends these records to your organisation's Govern deployment, the same service the Editor reports to.

By default each record holds **metadata and SHA-256 hashes only**. Your prompts, the model's answers, file contents, diffs, terminal output, setting values and your environment are not sent.

Capture is **off** unless your organisation's policy turns it on or you turn it on yourself. It never runs while you are signed out of Govern, unless your organisation runs its own receiver.

Run `neuralinverse activity` (or `/activity` in a session) at any time to see whether capture is on, why, and exactly what is recorded about you.

## When capture is on

The CLI decides in this order; `neuralinverse activity status` says which rule applied.

1. **No destination: off.** Capture is off while you are signed out, unless a self-hosted endpoint and token command are configured, or an embedding host takes the events.
2. **`activity.required: true` in managed policy: on,** and locked; you cannot turn it off.
3. **`activity.enabled: false` in managed policy: off,** and locked.
4. **Your own `activity.enabled: false`: off.**
5. **`activity.enabled: true` in managed policy: on.** This is a default you may opt out of with step 4.
6. **Your own `activity.enabled: true`: on.**
7. Otherwise **off.**

"Managed policy" means any managed layer: `managed-settings.json`, a remote policy, or your organisation's signed Govern policy.

## What is captured

The CLI posts events to `<Govern API>/api/v1/ingest/events`. The body is a JSON array of flat `snake_case` events, gzip-compressed. Every event carries this envelope:

| Field | Meaning |
|---|---|
| `type` | Event type (below) |
| `event_id` | UUIDv7, so resends can be deduplicated |
| `created_at` | ISO 8601 time |
| `app`, `app_version` | `"cli"` and the CLI version |
| `session_id` | Conversation id |
| `seq` | Sequence number within one CLI process |
| `correlation_id` | Shared by all events caused by one prompt |
| `turn_id` | One model call within the prompt |
| `device_id` | Your Govern device id |

The CLI never sends an organisation id or a user id; Govern derives both from your sign-in token.

| Type | When | Fields besides the envelope |
|---|---|---|
| `session_start`, `session_end` | A session opens or closes | Provider, model, permission mode, `cwd_hash` (SHA-256 of the workspace path), platform, content mode; at the end, duration and counts of prompts, tool calls and model calls |
| `prompt_submit` | You send a prompt | `prompt_hash` (SHA-256), length, number of attachments |
| `llm_call` | Every model call, including failed ones | Provider, model, `backend` (`cloud` for the organisation gateway, `byollm` otherwise), tokens, cache tokens, latency, time to first token, stop reason, retries, `error_class` (never the message), `request_hash` |
| `agent_action` | Every tool call, including denied ones | Tool, `action_type` (`file_write`, `file_read`, `terminal`, `browser`, `tool`), `payload_hash`, `approved`, `auto_approved`, `permission_source` (`rule`, `mode`, `user`, `hook`, `policy`, `default`), `approval_scope` (`once`, `always`), duration, status, `exit_code` for shell commands, workspace-relative `path` for file tools, `iteration` |
| `policy_enforcement` | A tool call is refused by a rule, the mode, a hook or organisation policy | `action: "agent_run"`, tool name, `decision: "deny"`, the source of the denial |
| `edit_applied` | A file tool changes a file | Path, `lines_added`, `lines_removed`, `diff_hash` |
| `edit_reverted` | An undo or restore restores files | The same, plus the checkpoint id |
| `checkpoint` | A restore point is created (one per prompt) | Checkpoint id |
| `compaction` | The conversation is compacted | Tokens before and after |
| `subagent_start`, `subagent_end` | A helper agent runs | Helper id, profile name, model, end reason, turns, duration |
| `mcp_connect` | An MCP server connects or fails | Server name, transport, state |
| `permission_mode_change` | The permission mode changes | From, to |
| `settings_change` | The CLI writes a settings file | Scope (`user`, `project`, `project-local`) and the **names** of the changed keys, never their values |
| `compliance_evidence` | Your organisation's policy changes during a session | `category: "policy_change"`, the policy hash, the changed key names |
| `capture_dropped` | Events were lost | Count, reason |

### Never captured

In any mode: terminal and tool output, setting values, environment variables, file contents, and paths outside the workspace (they appear only as `outside_workspace: true`).

### Raw content (organisation policy only)

An organisation can set `activity.content: "raw"` in its **managed** policy. Raw mode adds the prompt text, the model's response text, diffs and shell commands. Each field first passes the built-in secret scanner, which replaces every credential it recognises with `[REDACTED:<rule>]`, and is capped at 32 KB. `activity.content` in your own or a project's settings has no effect.

### Exclude paths

`activity.exclude` takes workspace-relative globs. Tool calls and edits that touch a matching path are not captured at all. A pattern without `/` matches a file name anywhere.

```json
{ "activity": { "exclude": ["secrets/**", "*.pem", "customer-data/**"] } }
```

## Inspect what is recorded

```bash
neuralinverse activity            # on or off and why, endpoint, queue size, last upload
neuralinverse activity log [n]    # the n oldest queued events as JSON, exactly as they will be sent
neuralinverse activity flush      # upload queued events now
neuralinverse activity purge      # delete queued events (capture continues if your organisation requires it)
```

In a session, `/activity` shows the status, `/activity log [n]` the events this session recorded, exactly as sent, and `/activity flush` uploads now.

## Delivery

- **Durable queue.** Events are appended to `~/.neuralinverse/activity/queue/<account>/` (folder 0700, files 0600) before anything is sent. The queue survives crashes, restarts and offline periods, and is kept per signed-in account, so events are never uploaded with another account's token.
- **Several processes.** Each CLI process appends to its own segment; one process at a time uploads, coordinated by a lock file.
- **Bounded.** Past 50 MB or 200,000 events (configurable), the oldest events are dropped and a `capture_dropped` event records how many.
- **Batches.** At most 500 events or 1 MB per request, gzip-compressed, with plain JSON as a fallback if the server refuses gzip.
- **Retries.** Network errors, 408, 429 and 5xx are retried with exponential backoff and jitter (1 second doubling to 5 minutes), honouring `Retry-After`. A batch is removed only after the server accepts it; a resend carries the same `event_id`s. A batch rejected for good (any other 4xx) is dropped and reported as `capture_dropped`.
- **Sign-in.** On a 401 the CLI refreshes your token once and resends. If that fails, uploads pause until you sign in again; the events stay queued.
- **Shutdown.** When a session ends, the CLI tries to upload for up to 2 seconds. The rest is sent by the next session or by `activity flush`.

## For administrators

To turn capture on for everyone signed in to your organisation, put this in the managed policy:

```json
{
  "activity": {
    "required": true,
    "exclude": ["**/*.env", "secrets/**"]
  }
}
```

Use `"enabled": true` instead of `"required": true` to make capture the default while letting people opt out. Add `"content": "raw"` only if your organisation needs prompts and diffs, and has told its developers.

| Key | Default | Meaning |
|---|---|---|
| `activity.enabled` | off | Turn capture on or off (see the order above) |
| `activity.required` | false | Managed only: always on |
| `activity.content` | `metadata` | Managed only: `raw` adds redacted prompts, responses, diffs and commands |
| `activity.endpoint` | `<Govern API>/api/v1/ingest/events` | Ingest URL (https; plain http only for localhost) |
| `activity.tokenCommand` | none | Self-hosted receiver: a command that prints the bearer token |
| `activity.exclude` | none | Workspace-relative globs that are never captured |
| `activity.maxQueueBytes` | 52428800 (50 MB) | Local queue bound |
| `activity.maxQueueEvents` | 200000 | Local queue bound |

### Self-hosted receivers

To send events somewhere other than Govern, set both `activity.endpoint` and `activity.tokenCommand`. The first line the command prints is used as the bearer token; it is cached for five minutes and run again after a 401. This works while signed out too. The receiver gets exactly the requests Govern would.

### Inside a governing host

When the CLI runs inside a host such as the Editor's shared agent host, events go to the host instead of the CLI's queue, with the same envelope and privacy rules. Capture is then on unless your settings or managed policy turn it off. The host applies your organisation's policy and forwards the events with its own sign-in.

## Known gaps

On the Govern side, the current ingest route `POST /api/v1/ingest/events` runs on the telemetry plane's own listener, is protected by a service token rather than a person's access token, and expects an object of the form `{"events": [...]}` rather than the CLI's flat, gzip-compressed array. Several of the CLI's event types (for example `prompt_submit`, `edit_applied`, `capture_dropped`) are not among the types the ingest plane lists. Delivery of the CLI's activity to Govern is therefore not confirmed end to end; events stay queued locally when the server refuses them for a retryable reason, and are dropped and reported as `capture_dropped` when refused for good. A self-hosted receiver works as described above.

Separately, the CLI writes its own [audit log](https://neuralinverse.com/docs/cli/enterprise/#audit-log) on the machine, which does not depend on Govern.
