Events

All sandbox activity is observable through the event stream, sandbox.getEventsStream(): a chronological, real-time record of everything the sandbox does. It doubles as an audit log, every command, file access, and network request the agent makes shows up here.

Streaming events

index.ts
import { getOrCreateSandbox } from "@hiver.sh/client";

const sandbox = await getOrCreateSandbox("my-sandbox");

const ac = new AbortController();

for await (const event of sandbox.getEventsStream({ signal: ac.signal })) {
  console.log(event.id, event.timestamp, event.type);
}

// Stop the stream
ac.abort();

The stream auto-reconnects on network drops (exponential backoff). Pass a signal to stop it cleanly.

Event types

EventFieldsDescription
config.applysuccess, changes, errorMessage?Sandbox config changed
egress.requestaccess, host, method, path, query?, headers?, body?Outbound HTTP request
egress.responserequest_id, status, duration_ms, headers?Response to an egress request
egress.chunkrequest_id, body, label?Streaming/WebSocket frame (label: up or down)
ingress.requestport, method, path, query?, headers?, body?Inbound request to a port exposed by the sandbox
ingress.responserequest_id, status, duration_ms, headers?Response to an ingress request (status + headers at time-to-first-byte)
ingress.chunkrequest_id, body, label?Inbound response body/WebSocket frame as it streams (label: up or down)
fs.requestaccess, mount, path, operationFile access attempt (read or write)
fs.responsebackend, request_id, duration_ms, error?File operation result
stdiostdout?, stderr?Process output
resource.usagecpu_percent, memory_bytesPeriodic resource sample
exec.requestcwd, commandExec call started
exec.responserequest_idExec call completed
system.startThe request to start the VM or container was received
system.config-changedconfigConfig was updated; config is the new config
system.vm-resumedA microVM was resumed from a snapshot instead of cold booting
system.shutdownThe sandbox expired its TTL without activity and is shutting down
system.fs-sync-requestbackend, operation, pathRequest to an external file-system backend (e.g. GCS, S3) is starting
system.fs-sync-responserequest_id, duration_ms, error?The external-backend request completed

access on egress.request and fs.request is "allowed" or "denied".

fs.request/fs.response describe the agent's file operations against a mount. system.fs-sync-request/system.fs-sync-response are one level down: they fire around each request the FUSE layer makes to an external backend (Google Cloud Storage, S3, Google Drive, OneDrive, Azure, or an external HTTP host) to sync a path — operation is one of list, list-dir, stat, get, put, delete, or move. Purely local mounts have no external backend and emit none. Pair a response to its request via request_id, which matches the request event's id.

Restricting which events are observed

By default every event type above is emitted. Set SandboxConfig.events to observe only a subset — useful to cut noise, or to avoid the overhead of capturing high-volume traffic (like ingress.chunk/egress.chunk bodies) you don't plan to read. An empty array observes nothing. events is reconciled at runtime, like fs and egress.

index.ts
const sandbox = await getOrCreateSandbox("my-sandbox", {
  events: ["egress.request", "egress.response", "exec.request", "exec.response", "stdio"],
});

This isn't just a display filter: an excluded event type's underlying work is skipped too. For example, narrowing events to exclude ingress.chunk stops the sandbox from capturing the response body of proxied inbound requests at all, rather than capturing it and discarding it unread.

Resuming from a cursor

Each event has a numeric id. Pass lastEventId to resume from where you left off, for example after a reconnect or process restart:

index.ts
let cursor: number | undefined;

for await (const event of sandbox.getEventsStream({ signal: ac.signal })) {
  cursor = event.id;
}

// Later, replay only events after the saved cursor
for await (const event of sandbox.getEventsStream({ lastEventId: cursor })) {
  // only events after id `cursor`
}

Filtering client-side

Filter events by checking event.type in the loop — useful for ad hoc filtering that doesn't need the overhead savings of SandboxConfig.events above:

index.ts
for await (const event of sandbox.getEventsStream()) {
  if (event.type === "egress.request" && event.access === "denied") {
    console.log("blocked:", event.host, event.method, event.path);
  }

  if (event.type === "fs.request" && event.access === "denied") {
    console.log("file denied:", event.path, event.operation);
  }
}

From the CLI

hiver events <sandbox-key> streams the same events to your terminal as JSON, one per line:

hiver events my-sandbox            # replay history, then stream live
hiver events my-sandbox --follow   # keep streaming and reconnect on close

Pass --jq <filter> to run each event through a jq expression before it's printed. The filter is applied to one event at a time, so select(...) drops events, .field projects, and a filter that yields multiple values prints one line each — the same as piping to jq, but built in:

# only denied egress requests
hiver events my-sandbox --jq 'select(.type == "egress.request" and .access == "denied")'

# a set of event types
hiver events my-sandbox --jq 'select(.type | IN("exec.request","fs.request","stdio"))'

# just the command of each exec call
hiver events my-sandbox --jq 'select(.type == "exec.request") | .command'

A non-matching select produces no output, so those events are dropped from the stream. Wrap the filter in single quotes so the shell leaves the double quotes inside intact. An invalid filter fails fast with a syntax error; a filter that errors on a particular event is reported to stderr while the stream keeps going.

Because the output is a JSON stream, you can also pipe it straight into another tool — for example, into an LLM for a plain-English summary:

hiver events my-sandbox \
  --jq 'select(.type | IN("exec.request","egress.request","fs.request","stdio"))' \
  | claude -p "what did the agent do?"