> ## Documentation Index
> Fetch the complete documentation index at: https://docs.opal.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Report agent activity

> **Prototype (AG-285).** Reports activity observed by an agent runtime on a developer's machine - today, Claude Code hook events.

This is **telemetry from cooperating machines, not attestation**. Opal can only see what a configured, consenting client chooses to send, cannot verify that a fleet is configured at all, and has no way to detect a machine that is reporting nothing. Absence of activity for an agent therefore means "nothing was reported", never "the agent did nothing".

The request body is either a batch envelope (`{"events": [...]}`), a bare JSON array of events, or a single raw Claude Code hook payload - the last so a hook can POST here directly with no relay in between.

The bearer token establishes the **organization** and nothing finer. No field in the payload is treated as an identity claim Opal trusts: the actor headers below are used for best-effort attribution and display only. Events that cannot be attributed to an agent are retained and surfaced as unattributed activity rather than dropped.



## OpenAPI

````yaml https://app.opal.dev/openapi.yaml post /agent-activity
openapi: 3.1.0
info:
  contact:
    email: hello@opal.dev
    name: Opal Team
    url: https://www.opal.dev/
  description: >-
    The Opal API is a RESTful API that allows you to interact with the Opal
    Security platform programmatically.
  title: Opal API
  version: '1.0'
servers:
  - description: Production
    url: https://api.opal.dev/v1
security: []
tags:
  - name: access-rules
    description: Operations related to access rules
  - name: agent-activity
    description: Operations related to agent activity telemetry
  - name: apps
    description: Operations related to apps
  - name: bundles
    description: Operations related to bundles
  - name: campaigns
    description: Operations related to access review campaigns
  - name: configuration-templates
    description: Operations related to configuration templates
  - name: delegations
    description: Operations related to request reviewer delegations
  - name: event-streams
    description: Operations related to event streaming connections
  - name: events
    description: Operations related to events
  - name: groups
    description: Operations related to groups
  - name: group-bindings
    description: Operations related to group bindings
  - name: idp-group-mappings
    description: Operations related to IDP group mappings
  - name: message-channels
    description: Operations related to message channels
  - name: non-human-identities
    description: Operations related to non-human identities
  - name: on-call-schedules
    description: Operations related to on-call schedules
  - name: opal-queries
    description: Operations related to OpalQuery
  - name: opal-scripts
    description: Operations related to OpalScripts
  - name: owners
    description: Operations related to owners
  - name: requests
    description: Operations related to requests
  - name: resources
    description: Operations related to resources
  - name: role-assignments
    description: >-
      Operations related to role assignments (general grants between an entity
      and a principal)
  - name: paladin
    description: Operations related to Paladin
  - name: service-users
    description: Operations related to service users (non-human identities owned by a team)
  - name: sessions
    description: Operations related to sessions
  - name: tags
    description: Operations related to tags
  - name: tokens
    description: Operations related to API tokens
  - name: uars
    description: Operations related to UARs. Deprecated in favor of the `campaigns` API.
  - name: users
    description: Operations related to users
paths:
  /agent-activity:
    post:
      tags:
        - agent-activity
      summary: Report agent activity
      description: >-
        **Prototype (AG-285).** Reports activity observed by an agent runtime on
        a developer's machine - today, Claude Code hook events.


        This is **telemetry from cooperating machines, not attestation**. Opal
        can only see what a configured, consenting client chooses to send,
        cannot verify that a fleet is configured at all, and has no way to
        detect a machine that is reporting nothing. Absence of activity for an
        agent therefore means "nothing was reported", never "the agent did
        nothing".


        The request body is either a batch envelope (`{"events": [...]}`), a
        bare JSON array of events, or a single raw Claude Code hook payload -
        the last so a hook can POST here directly with no relay in between.


        The bearer token establishes the **organization** and nothing finer. No
        field in the payload is treated as an identity claim Opal trusts: the
        actor headers below are used for best-effort attribution and display
        only. Events that cannot be attributed to an agent are retained and
        surfaced as unattributed activity rather than dropped.
      operationId: reportAgentActivity
      parameters:
        - description: >-
            The identity the reporting client claims this activity belongs to -
            an email or an Anthropic user id. **Unverified**: a Claude Code hook
            payload contains no user identity, so this has to be injected by
            whoever deploys the hook (via the http handler's `headers` map and
            `allowedEnvVars` interpolation), and whoever controls the machine
            controls the header. Used only when the API key itself does not
            identify a developer. When the email belongs to a person in the
            organization, that person is recorded as who the activity was
            reported for (always as a claim), and the activity is attributed to
            the one Claude agent they own, if they own exactly one. The API
            key's own email is never treated this way: a key belongs to whoever
            created it, which with a shared key is not who is at the keyboard.
            Omitting it is valid. When an event's `opal_reported` object carries
            other emails, Opal also tries them, in the order `configured_email`,
            `claude_account_email`, `git_email`, after this header and skipping
            any that repeat it. The first one that resolves to an agent wins.
          explode: false
          in: header
          name: X-Opal-Actor
          required: false
          schema:
            type: string
          style: simple
        - description: >-
            The name of the machine that sent this request, as that machine
            reports it (`hostname -s`). **Unverified**: two machines can share a
            name, and whoever controls the machine controls the header. Stored
            with each event, so a reader can tell which machine reported it.
            When an event's own `opal_reported.hostname` names a different
            machine, this header wins and the body's value is kept beside it.
            Omitting it is valid.
          explode: false
          in: header
          name: X-Opal-Host
          required: false
          schema:
            type: string
          style: simple
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentActivityIngestRequest'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentActivityIngestResult'
          description: >-
            A per-batch accounting of what was accepted, stored, and attributed.
            An organization that has agent activity turned off gets a 200 with
            an empty body for every body shape, so a sender does not keep the
            events and try again.
        '400':
          description: >-
            The body was unparseable, empty, or carried more events than one
            request may.
        '413':
          description: The request body exceeded the maximum accepted size.
      security:
        - BearerAuth: []
components:
  schemas:
    AgentActivityIngestRequest:
      description: >-
        # AgentActivityIngestRequest Object

        ### Description

        A batch of agent activity events.


        Three body shapes are accepted: this envelope (`{"events": [...]}`), a
        bare JSON

        array of `AgentActivityEvent`, and a single `AgentActivityEvent` object.
        The last

        exists so a Claude Code hook can POST here directly, since a hook sends
        one bare

        payload per event and cannot wrap it.
      properties:
        events:
          items:
            $ref: '#/components/schemas/AgentActivityEvent'
          type: array
      type: object
      additionalProperties: true
    AgentActivityIngestResult:
      description: >-
        # AgentActivityIngestResult Object

        ### Description

        What one ingest request actually did.


        `received` and `stored` differ when a delivery was retried and
        deduplicated.

        `attributed` and `unattributed` sum to `received` rather than to
        `stored`, because

        attribution happens before deduplication. Unattributed events are kept,
        not dropped:

        activity Opal cannot tie to a governed agent is itself a governance
        finding.
      properties:
        received:
          description: Events parsed out of the request body.
          type: integer
        stored:
          description: >-
            Rows actually written. Lower than `received` when a retried delivery
            was deduplicated on `tool_use_id`.
          type: integer
        attributed:
          description: Events that resolved to an agent in this organization.
          type: integer
        unattributed:
          description: >-
            Events that resolved to no agent, and are therefore visible only in
            the unattributed activity list. A persistently high value means the
            reporting clients are not sending an identity Opal can resolve.
          type: integer
      required:
        - received
        - stored
        - attributed
        - unattributed
      type: object
    AgentActivityEvent:
      description: >-
        # AgentActivityEvent Object

        ### Description

        One activity event reported by an agent runtime. Today this is a Claude
        Code hook

        payload, posted either by the hook itself or by a relay that batches
        them.


        Only the fields below are read; everything else in the payload is parsed
        and

        **discarded**, never persisted. That still includes `tool_response` (the
        result

        body, which carries file contents and third-party workspace data) and

        `transcript_path` / `scratchpad_dir` (absolute paths on an employee's
        own laptop,

        which Opal cannot govern and should not be accumulating).


        `tool_input` IS now read and stored. It was previously discarded pending
        a

        data-handling decision; that decision has since been made, and without
        arguments

        no policy can name what a call acted on - a rule about a file path, a
        command

        family or a web domain refers to something that exists only inside
        `tool_input`.
      properties:
        hook_event_name:
          description: >-
            Which hook fired, e.g. `PostToolUse`, `PreToolUse`,
            `PermissionDenied`. Required; an event with no name cannot be
            interpreted and is rejected.
          type: string
        tool_input:
          description: >-
            The call's arguments, as the runtime sent them. Server-defined for
            an MCP tool;

            a fixed shape per built-in (`command` for Bash, `file_path` for
            Edit, `url` for

            WebFetch).


            Stored as sent, and separately reduced to a normalized target the
            activity feed

            and policy can match on. The stored copy is the record of fact; the
            derived

            target is recomputed whenever the extractor improves.


            Capped before storage. A capped payload is no longer valid JSON, so
            the row

            records that it was capped rather than leaving each reader to
            discover it by

            failing to parse.
          type: object
          additionalProperties: true
        session_id:
          description: The agent session this event belongs to. Opaque to Opal.
          type: string
        prompt_id:
          description: >-
            Groups every tool call made under one user instruction, so a run can
            be read back as the instruction that caused it rather than as
            unrelated rows.
          type: string
        agent_id:
          description: >-
            Set only when the call came from an autonomous subagent rather than
            the session a person is typing into. Stored as `subagent_id` -
            Opal's own `agent_id` already means something else.
          type: string
        agent_type:
          description: >-
            The subagent's type, e.g. `general-purpose`. Set alongside
            `agent_id`.
          type: string
        permission_mode:
          description: >-
            The session's permission mode, e.g. `default`, `acceptEdits`,
            `bypassPermissions`. A session running `bypassPermissions` had no
            approval gate in front of any tool call it made.
          type: string
        actor_email:
          description: >-
            The identity the sender reports for this specific event.
            **Unverified**, the same as `X-Opal-Actor`, and used only when
            neither the API key nor that header identified a developer. An
            `http` hook cannot populate this; a `command`-type hook or a relay
            can (`$USER`, a hostname, `git config user.email`), which is why it
            is per-event rather than per-request - a relay can batch several
            developers.
          type: string
        opal_reported:
          $ref: '#/components/schemas/AgentActivityOpalReported'
        duration_ms:
          description: >-
            How long the tool call took to run, in milliseconds, as Claude Code
            measured it. Claude Code's documentation says it leaves out time
            spent in permission prompts and in `PreToolUse` hooks. It is sent
            with `PostToolUse` and `PostToolUseFailure` only; it is ignored on
            any other event. **Unverified.** Stored only when it is a number
            from 0 to 86400000 (24 hours), rounded to a whole millisecond; any
            other value is dropped and the event is stored without it. A value
            of the wrong type never rejects the event or the batch it came in.
          x-go-type: json.RawMessage
        transcript_path:
          description: >-
            Read at ingest **only** to parse an operating-system username out
            of, as the last-resort attribution step, and then discarded. The
            path itself is never stored: it is an absolute path on an employee's
            laptop and Opal has no business accumulating those.
          type: string
        tool_name:
          description: >-
            The tool invoked, for tool-scoped events. Empty for session-level
            events.


            An MCP tool is named `mcp__<server>__<tool>`. The whole string is
            stored as sent,

            and the server and the tool are also stored in their own fields (see
            `mcp_server`).
          type: string
        mcp_server:
          $ref: '#/components/schemas/AgentActivityMcpServer'
        tool_use_id:
          description: >-
            The tool invocation's id, used together with `hook_event_name` to
            deduplicate a retried delivery. Absent on events that carry none,
            which are stored at-least-once.
          type: string
        cwd:
          description: The working directory the agent was operating in.
          type: string
        timestamp:
          description: >-
            When the client says the event occurred (RFC 3339). Clamped
            server-side to a window around receipt, because a client-controlled
            sort key is otherwise a way to pin rows to the top of every list.
            Defaults to receipt time when absent or unparseable.
          format: date-time
          type: string
      required:
        - hook_event_name
      type: object
      additionalProperties: true
    AgentActivityOpalReported:
      description: >-
        # AgentActivityOpalReported Object

        ### Description

        What the sending machine says about who and what it is, added by the
        Opal hook

        script. Every field is optional and **unverified**: whoever controls the
        machine

        controls them. They can disagree, and Opal keeps every one that is
        present, each cut

        to a fixed length (254 characters for an email, 64 for `os_user`, 255
        for

        `hostname`) with control characters removed.


        The emails are also tried, in the order below and after the
        `X-Opal-Actor` header,

        to find the agent, so a person without a git email can still match
        through their

        Claude account.
      properties:
        configured_email:
          description: An email set by an administrator, an MDM profile or the installer.
          type: string
        claude_account_email:
          description: The email of the account Claude Code is signed in with.
          type: string
        git_email:
          description: The machine's global `git config user.email`.
          type: string
        os_user:
          description: The operating-system username. Not used to find an agent.
          type: string
        hostname:
          description: >-
            The machine's name. The `X-Opal-Host` header wins when it names a
            different machine.
          type: string
      type: object
    AgentActivityMcpServer:
      description: >-
        # AgentActivityMcpServer Object

        ### Description

        The MCP server behind a tool call, as Claude Code reports it. Claude
        Code v2.1.274 and

        later adds this object to the hook input of an MCP tool call; an older
        client does not

        send it, and it is never present for a built-in tool such as `Bash`.


        When `name` is present it is stored as the call's server. When this
        object is absent,

        the server is read from `tool_name` instead: the part after the `mcp__`
        prefix and

        before the next `__`. The tool is always read from `tool_name`.


        `name` is stored as the call's server. `source` says where the server's
        definition came

        from (`plugin`, `sdk`, or a configuration scope such as `user` or
        `project`) and is

        stored as sent, cut to 64 characters. Neither field is an identity:
        `name` is whatever

        the user called the server in their own configuration, and `source` is
        what the machine

        said.
      properties:
        name:
          description: The server's name.
          type: string
        source:
          description: >-
            Where the server's definition came from, such as `plugin`, `sdk`,
            `user` or `project`. Stored as sent, cut to 64 characters.
          type: string
      type: object
  securitySchemes:
    BearerAuth:
      scheme: bearer
      type: http

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.