@primitivedotdev/cli
v1.34.0
Published
Official Primitive CLI: deploy Primitive Functions, send and inspect mail, manage endpoints, all from the terminal. Wraps the @primitivedotdev/sdk runtime client with one-shot commands.
Readme
primitive
Official Primitive CLI. Deploy Primitive Functions, send and inspect mail, manage endpoints, all from the terminal.
brew install primitivedotdev/tap/primitive
primitive whoamiOr with npm:
npm install -g primitive
primitive whoami
# `prim` is installed as a short alias for the same CLI.
prim whoamiThe same CLI is also published as primcli and under the legacy scoped name @primitivedotdev/cli. Each installs an identical build with the same primitive/prim commands. Use whichever name you prefer; they track the same version.
Or with no install:
npx primitive@latest <command>This package wraps the @primitivedotdev/sdk runtime client with one-shot commands. For in-handler use (calling Primitive from inside a Function), import createPrimitiveClient from @primitivedotdev/sdk/api directly; the CLI is for operator and deploy workflows.
Quickstart
Receive webhook events without a public endpoint
With an existing Primitive account and inbox, sign in or set PRIMITIVE_API_KEY.
Run primitive listen to print existing webhook events as JSONL.
--json is accepted explicitly; status remains JSON and stdout events remain JSONL. Status goes to
stderr. No public URL or separate destination setup is required.
For an agent, use a short hook that saves each event into its durable inbox:
primitive listen init --language python
cd primitive-listener
primitive listen --subscription my-agent --exec 'python3 accept_event.py'The starter works locally and refuses to overwrite an existing directory. Its
SQLite inbox is application-owned example code. Replace accept_event with your
agent's existing durable input function. Exit 0 means the input was saved; run
model work separately so the listener can keep receiving while the agent thinks.
The hook has a 30-second deadline. Do not spawn a background agent from the hook.
--exec requires macOS or Linux for process-group cleanup. On Windows, use
--forward-to with your agent's local HTTP acceptance handler.
The CLI passes the unchanged event body on stdin, plus PRIMITIVE_EVENT_ID,
PRIMITIVE_DELIVERY_ID, and PRIMITIVE_EVENT_TYPE in the hook environment.
Deduplicate by PRIMITIVE_EVENT_ID, which stays the same on redelivery. Delivery
IDs identify individual attempts. Related events need not contain an email or a
body-level event ID. The generated processing example shows existing Python SDK
conversation and reply calls; sending requires an explicit choice.
To run an existing HTTP webhook handler locally:
primitive listen --subscription local-webhook --forward-to http://localhost:3000/webhookThis mode forwards the exact signed body and existing webhook headers. Configure the handler with your existing account webhook secret and normal signature verification. The CLI never forwards your API credentials. It does not follow redirects or disable TLS verification. A response must finish within 30 seconds and remain within 1 MiB. Existing confirmation headers retain their meaning, including account content-discard behavior; exec and stdout do not confirm content discard.
Use --events email.received,interaction.ack.received to select events on a new
subscription. Omitting --events when resuming preserves its selection. A
conflicting selection requires a different subscription name. Ctrl-C disconnects
without deleting the destination; restart with the same name to receive pending
events. On the same machine, a second listener cannot consume that subscription
while the first holds it. The same name on different machines shares consumption;
different names receive independent copies. With no name, the CLI saves a private
default identity scoped to your API host and account.
--number N exits after N successful, server-confirmed completions. A crash after
the hook saves input but before completion can redeliver it. Bare stdout is for
inspection: a successful pipe write does not prove a downstream agent saved the
event. Use the durable hook for ingestion.
The server retains pending references for 24 hours, subject to source-content
availability and queue capacity. The listener reports delivery gaps instead of
silently claiming it is caught up. Saving download links does not preserve
attachment bytes or extend their expiry. To remove a destination, use its ID from
the readiness message: primitive endpoints delete --id DESTINATION_ID.
Low-level delivery API
The generated endpoints pull-webhook-event and endpoints complete-webhook-event
commands expose the local-delivery API when enabled on your API host. These are
single API calls; a receive loop must retain the destination identity and report
each attempt's outcome. Use --help to inspect the request fields:
primitive endpoints pull-webhook-event --help
primitive endpoints complete-webhook-event --helpCompletion uses a mode-specific JSON body. For a successful process handler:
primitive endpoints complete-webhook-event --id ENDPOINT_ID --raw-body '{"queue_id":"QUEUE_ID","delivery_id":"DELIVERY_ID","lease_token":"LEASE_TOKEN","mode":"exec","exit_code":0,"duration_ms":12}'Use the IDs and lease token returned by pull. HTTP forwarding reports
mode: "http" with status_code; stdout reports mode: "stdout" with
write_succeeded. Transport failures must also be reported. Successful handler
acceptance is separate from any later agent reasoning.
Pull delivery preserves existing webhook bodies and supplies canonical event and attempt IDs separately. Deduplicate by event ID. A successful completion can be retried with the same queue, attempt, token, and outcome. Check the returned gap count as well as the backlog; queue retention does not extend email-content retention. Conversation retrieval and threaded replies use the existing SDK API.
primitive signin
primitive whoami
primitive functions templates
primitive functions init my-fn
cd my-fn && npm install && npm run build
primitive functions deploy --name my-fn --file ./dist/handler.js
primitive send --to [email protected] --body "Hello!" --wait
primitive emails latest --limit 5
primitive inbox nextRun primitive --help for the full command list. Per-command help (primitive functions deploy --help) carries enough detail that an agent can compose any operation without leaving the terminal.
Waiting with connected credentials
After sending, wait for the existing send's reply without sending again:
primitive emails wait --reply-to-sent-email-id <sent-id> --from [email protected]The CLI reads that sent record to derive your receiving address. Optional --to
must match it. The peer stays explicit because sent records do not expose a
complete recipient inventory. Connected waits include existing replies by
default, so fast replies are not missed; use --since <timestamp> to narrow the
window. They require an authenticated peer and the exact outbound parent.
Interaction attachments remain pending with inspection guidance. JSONL includes
matching email details, or use --table for compact output and --number N for
multiple replies. Timeout exits 1; repeat the same wait to recover without
resending. Connected waits share one local address event receiver and recover
through exact-parent search, without scanning inbox history. Additional content
filters on this command require organization credentials.
Connected primitive chat <peer> <message> --from <own-address> registers its
reply wait and authenticates the receiver before sending. It records an explicit
idempotency key before the request. If the send response is lost, repeating the
same command looks up that key without sending again. A timeout retains the exact
parent claim for resume. A plain reply is an email response, not proof that a task
is complete.
Authentication
Use primitive signin or primitive login for existing accounts. With no email, both use browser approval; primitive signin browser and primitive login browser are the explicit browser forms.
Use primitive signin <email> --signup-code <code> --accept-terms, then primitive signin confirm <email> <code> for email-code sign-in. primitive login <email> and primitive otp <email> support the same email-code flow with matching confirm and resend subcommands.
Use primitive logout --force to remove local CLI credentials, pending email-code auth state, and stale credential locks without contacting Primitive. This is the recovery command when an interrupted auth command leaves the CLI saying another credential operation is already in progress.
Use primitive signup <email> for new account creation, then primitive signup confirm <email> <code> with the emailed verification code. Non-interactive signup is available with --accept-terms (pass --signup-code <code> too if you have one).
Reply state and the agent loop
Every inbound email carries reply state: awaiting is you when the latest
message in its thread is inbound (it waits on your reply) and them when you
replied last; reply_count and last_replied_at describe replies to that one
email. A reply counts once it is sent or committed to go (queued and scheduled
count; gate-denied, agent-failed and canceled sends do not).
primitive emails latest shows it as the AWAITING and REPLIES columns, and
--json carries the fields. Filter on it with --awaiting you|them on
emails latest, emails list, emails search, emails wait, emails watch
and search, or with awaiting:you in a search query.
primitive inbox next returns the oldest email awaiting your reply, its
conversation (roles user and assistant; the API caps long threads and sets
truncated when older messages are omitted), an automated verdict with
reasons, and the exact primitive reply --id <id> command that answers it. The
loop is:
primitive inbox next --json > next.json # exit 5: nothing awaits you
primitive reply --id "$(jq -r .email.id next.json)" --body "..."
primitive inbox next --json # the next one| Exit | Meaning |
|---|---|
| 0 | An email awaits your reply; it is printed. |
| 1 | Error, including reply_state_unsupported or automated_filter_unsupported from an older server. |
| 2 | Invalid flags or arguments. |
| 5 | Nothing awaits your reply (with --wait: still nothing at --timeout). |
- Automated mail is skipped unless you pass
--include-automated: null envelope sender (bounces), mailer-daemon and postmaster, mail sent from the very address it was delivered to, delivery, feedback and disposition reports, and mail that declares itself automated (Auto-Submitted, Precedence bulk/list/junk, List-Unsubscribe, List-Id, X-Auto-Response-Suppress, X-Failed-Recipients). This is the same set Primitive's own automatic responders decline to answer. The API decides this when the mail arrives (automated,automated_reasonson every email) andinbox nextfilters on it server-side (awaiting=you&automated=false), so a call costs the same however much unanswered automated mail has piled up. On an empty result,automated_awaitingsays how much automated mail also awaits.automation_headers_known: falsemeans the email had no automation headers on record (none declared, or received before they were captured), soautomated: falserests on the sender checks alone. Filter on the verdict yourself with--automated true|falseonemails list,emails search,emails waitandemails watch, orautomated:falsein a search query. - The
awaitingfilter covers delivered mail only: mail the server rejected (for example over the storage limit) was never delivered and is never returned as awaiting you. A server whose filter still returns rejected mail fails withawaiting_rejected_unsupported. --wait [--timeout N]blocks until something awaits you (default 300 seconds, 0 waits forever). It reads the inbox's newest position before checking reply state, then long-polls from that position and re-checks on every arrival and at least every 30 seconds, so mail that lands between the check and the wait is not missed. Mail that arrives while you compose a reply is stillawaiting=youon the next call, because the state lives on the server, not in a cursor.- The email carries
from_known_addressandauth(SPF, DMARC) so an agent can weigh instructions in it; the transcript prints them and strips terminal control sequences from sender-supplied text. - It is not a work queue. Nothing is claimed or locked, so two agents calling
inbox nexton the same inbox get the same email until one replies. Run one agent per inbox. - Against a server without the
automatedfilter,inbox nextfails withautomated_filter_unsupportedrather than deciding automated mail itself and re-reading all of it on every call;--include-automatedstill works there. - Against a server that does not report reply state,
inbox nextand every--awaitingfilter fail withreply_state_unsupportedinstead of treating mail as unanswered.
Command style
Use task-oriented commands for normal workflows:
primitive send --to [email protected] --body "Hello"
primitive reply --id <inbound-email-id> --body "Thanks"
primitive reply --id <inbound-email-id> --body "See attached" --attachment ./report.pdf
primitive chat reply "See attached" --attachment ./report.pdf
primitive emails list
primitive emails get --id <inbound-email-id>
primitive sent list
primitive sent delete --id <sent-email-id>
primitive domains list
primitive functions templates
primitive functions init my-fn --template email-reply
primitive functions logs --id <function-id>
primitive memories set thread:latest '{"email_id":"em_123"}'
primitive memories get thread:latest
primitive deliveries replay --id <delivery-id>Generated API commands remain available for compatibility and full schema parity, for example primitive emails:list-emails and primitive sending:reply-to-email.
Send outcomes and exit codes
primitive chat, primitive chat reply, primitive send and primitive reply
report the same outcomes. Exit codes tell you whether a message left and whether
sending again is safe. With --json, stdout is an envelope for every outcome
(failures included) whose outcome field carries the name.
| Outcome | Exit | Meaning |
|---|---|---|
| replied | 0 | Chat only: the message was sent and a reply arrived. |
| sent | 0 | Accepted for delivery. status: "queued" is a success, not a pending failure. |
| already_sent | 0 | The server recognised an identical earlier send, or refused with HTTP 410 sent_email_deleted because that earlier send was deleted. Nothing new went out. Do not resend. |
| not_sent | 1 | The API rejected the request (HTTP 400, 401, 402, 403, 404, 413, 422 or 429), the command failed before sending, or the send record has status agent_failed, gate_denied or canceled. Nothing went out. |
| (usage error) | 2 | Invalid flags or arguments. Nothing went out. |
| sent_awaiting_reply | 3 | Chat only: the message was sent but no reply arrived before --timeout. Wait with the printed command; do not resend. |
| uncertain | 4 | Transport error, conflict, server error, or a send record with status unknown. The message may or may not have gone out; check primitive sent list before retrying. |
A chat that times out prints Message sent (id X). No reply yet after Ns. Do NOT
resend; wait with: <command>, and its --json envelope has "reply": null, the
sent record, and follow_up_commands that only wait on or inspect that send.
Without --json, send and reply keep printing the send record on stdout exactly
as before and add a one-line stderr summary such as Reply sent (queued for
delivery, id X). Do not resend. Before sending, primitive reply (and
primitive chat --reply) warns on stderr when the inbound email already has a reply
that went out. The warning never blocks the send; if the lookup fails, the reply is
still sent and stderr says the check was skipped. --json includes the replies as
prior_replies.
Remove mailbox history
primitive sent delete --id <sent-email-id> removes sender history and owned
attachments. It does not recall delivery or delete recipient copies. Cancel
scheduled sends before deleting them. A 409 means the record is not currently
eligible; a 500 or 503 may mean some files were removed already, so retry the same
DELETE. Repeating a completed deletion succeeds.
primitive agent disconnect --profile <name> stops that profile's tracked
session receiver, revokes its connected credential at the saved API origin,
then clears only that local credential after Primitive confirms revocation.
Mail, notes, setup evidence and notification receipts remain. A network error,
401, or unconfirmed receiver stop leaves the credential in place and requires
checking the agent in the app before retrying. Foreground or external runtime
hooks are not managed by this command.
After disconnecting an agent, an owner or admin logged in with OAuth can run
primitive agent-connections remove-agent-connection --address [email protected].
This removes the revoked connection record while preserving mail, notes and the
external runtime. Active connections return 409; missing records return 404.
Organization API keys cannot remove connections.
Send and reply can return HTTP 410 sent_email_deleted when a prior send was
deleted. Its occupied idempotency key or automatic reply suppression stays reserved.
Do not generate a new key or send again to bypass this refusal.
Primitive Memories
Memories are durable JSON key-value records scoped to your org by default. Use
--function <function-id> to read or write the same key under a function scope;
the value is the function id UUID, not the function name.
primitive memories set thread:latest '{"email_id":"em_123"}'
primitive memories set greeting '"hello"'
primitive memories get thread:latest
primitive memories search thread: --metadata-only
primitive memories delete thread:latest
primitive memories set state '{"step":2}' --function <function-id>
primitive memories get state --function <function-id>Values must be valid JSON. Strings must be quoted as JSON strings, so use
'"hello"', not hello.
Credits
Redeem a credit code for your organization and check the credit balance.
primitive credits redeem LAUNCH50
primitive credits balance
primitive credits balance --jsoncredits redeem prints the credit added and its expiry. On a refusal it prints
the server's message (for example an invalid or already redeemed code) and
exits non-zero. Redeeming needs an organization owner or admin; with an API key,
the key's creator must currently be an owner or admin. Each run sends a new
Idempotency-Key; pass --idempotency-key <key> to retry the same redemption
safely. On any failure the command prints the key it used to stderr, so you can
run the same command again with --idempotency-key <key>.
Recipient routing
Bind a recipient address to a destination so inbound mail resolves to a single
endpoint. Pass --function to route an address to a function (its route-target
endpoint is created in the same call, enabling per-address routing like
[email protected] -> functionA), or --endpoint for an existing endpoint.
primitive routes add [email protected] --function <function-id>
primitive routes add 'support+*@acme.com' --match wildcard --endpoint <endpoint-id>
primitive routes list
primitive routes test [email protected] # preview where an address resolves, with the rule trace
primitive routes update <route-id> --priority 5
primitive routes reorder --set <route-id>=10 --set <other-id>=20
primitive routes remove <route-id>Recipient routing is gated by an organization entitlement; routes are inert until it is enabled.
x402 payments
The primitive payments command group drives non-custodial x402 USDC payments. One agent registers a payout address and requests a payment; the paying agent signs locally with its own wallet key and settles. The key never leaves your machine. Networks are base and base-sepolia. Amounts take a human USDC value (--amount-usdc 0.01) or token base units (--amount 10000, since USDC has 6 decimals). Your org is resolved automatically from your API key, so payout registration takes no org flag.
# Payee, one time: register the default address your org is paid at. Signs an
# ownership message locally with your wallet key. Org is auto-resolved.
primitive payments register-payout-address --network base-sepolia --label treasury
# Payee: request a payment with a human USDC amount. Prints the challenge JSON
# to stdout; a one-line summary goes to stderr.
primitive payments charge --network base-sepolia --amount-usdc 0.01
# Capture the challenge JSON to hand to the payer.
primitive payments charge --network base-sepolia --amount-usdc 0.01 > challenge.json
# Payer: sign and settle the challenge locally. Reads the challenge inline,
# from a file, or piped on stdin.
primitive payments pay --challenge-file challenge.json
cat challenge.json | primitive payments pay
# Email-native flow: the payee issues a challenge over an email thread.
# Note: create-email-challenge takes --amount in token base units only; unlike
# `charge` it has no --amount-usdc. USDC has 6 decimals, so multiply by
# 1,000,000: 0.01 USDC is --amount 10000.
primitive payments create-email-challenge --from [email protected] \
--to [email protected] --amount 10000 --network base-sepolia
# Payer (recommended): pay the email challenge in one step. Signs the challenge
# locally with your wallet key AND sends the signed interaction.json, so you
# skip the manual sign-then-send dance. --in-reply-to is the inbound challenge
# email you received; it is fetched to address the payment to the payee, with
# From defaulting to the payer it was sent to. The send is not threaded under
# the challenge (the payment associates by interaction_id). The message carries
# a short default note alongside the attachment; pass --body to customize it.
primitive payments pay-email --challenge-file challenge.json \
--in-reply-to <inbound-challenge-email-id> --wait
# Advanced: sign only, without sending. Emits the portable interaction.json
# artifact you can attach yourself (e.g. with `primitive send --attachment`).
primitive payments pay-email-step --challenge-file challenge.json > interaction.json
# Inspect a challenge by id, or list your registered payout addresses.
primitive payments get-challenge --id <challenge-id>
primitive payments list-payout-addresses
# Read and update the org spend policy (kill-switch, per-payment and daily caps,
# payee allowlist). The update merges: omitted fields keep their value.
primitive payments get-spend-policy
primitive payments update-spend-policy --max-per-payment 5000000charge is the friendly verb that matches the SDK charge and accepts --amount-usdc; create-challenge is the lower-level command that takes base-unit --amount. Either creates a challenge. All four signing commands accept --json. register-payout-address and pay print a human-readable summary by default and raw JSON with --json; pay-email and pay-email-step print JSON by default (the send result and the interaction.json bytes, respectively), and --json switches them to a fuller envelope object.
The signing commands (register-payout-address, pay, pay-email, and pay-email-step) need your wallet key. pay-email is the recommended payer path for the email-native flow: it signs and sends in one step. pay-email-step signs only and emits the interaction.json artifact for advanced use where you want to deliver it yourself. Set the key in PRIMITIVE_X402_PRIVATE_KEY (a 0x-prefixed hex private key) so it never lands in shell history or the process list:
export PRIMITIVE_X402_PRIVATE_KEY=0x...A --private-key flag is available as an escape hatch for scripted use, but the environment variable is preferred. The non-signing commands (charge, get-challenge, list-payout-addresses, get-spend-policy, update-spend-policy) need only your Primitive API key. Run primitive payments <command> --help for the full flag list of any command.
Migrating from @primitivedotdev/sdk CLI
The CLI previously shipped inside @primitivedotdev/sdk. The shipped surface area is identical; only the package name changes.
| Before | After |
|--------|-------|
| npm install -g @primitivedotdev/sdk | npm install -g primitive |
| npx @primitivedotdev/sdk@latest <cmd> | npx primitive@latest <cmd> |
@primitivedotdev/sdk continues to ship the runtime SDK (webhook, API client, contract, parser, openapi). Use it in your application code; use primitive in your shell and CI.
License
MIT
Local event listening
primitive listen --forward-to localhost:3000
primitive listen --forward-to 3000 --events email.received
primitive listen --once --timeout 60
primitive listen --subscription my-agent --exec "python3 accept.py"WebSocket is the default transport. Subscription registration and reconnects are
automatic; the saved default resumes the same durable queue. --once waits for
one successful, confirmed delivery. --timeout is in seconds and exits 2 on
timeout; Ctrl-C exits 130. Bare primitive listen prints one raw JSON event per
line. Use --transport poll explicitly for HTTP polling. Accept or enqueue each
event within 30 seconds. Closing preserves pending work, and retries can deliver
an event more than once.
Native session email notifications
With a connected-agent credential already configured, notify one exact loaded Codex session of authenticated mail from explicitly approved senders:
primitive listen --notify-session <session-uuid> --sender [email protected]
primitive listen --notify-session <session-uuid> --sender [email protected],[email protected]
primitive listen --background --notify-session <session-uuid> --contacts --contact-requests
primitive listen --status --notify-session <session-uuid> --json
primitive listen --status --notify-session <session-uuid> --email-id <received-id>
primitive listen --status --notify-session <session-uuid> --limit 100 --cursor <nextCursor>
primitive listen --stop --notify-session <session-uuid>Notifications arrive as external primitive.mail_received tool-output events,
never synthetic user messages. An idle session can wake to evaluate the notice;
a busy session receives it in its active turn. The notice does not grant user
authority or permission to execute requests from email.
This notification path uses turn/start with empty input and toolOutput over
the native local-session Unix socket in Codex. It requires runtime support for
external tool-output turns and has no user-message fallback. Live runtime behavior was verified on macOS; Linux uses the same
Unix transport but has not been verified against a live runtime. The session must already be open in a terminal with
native daemon support enabled, with compatible client and server versions.
The foreground command exits if its socket is unavailable; a background receiver
reports reconnecting and waits for a temporarily unavailable native socket.
Neither creates a subscription before native preflight succeeds. The CLI does
not launch a coding daemon, start/resume a conversation, install a
connector, or change model, approval, or sandbox settings. Windows and other
harnesses are not supported by this path. CODEX_HOME selects the native runtime
home when it differs from ~/.codex.
--background starts one detached CLI process for this connection and exact
session. It survives exit of the process that started it. Repeating the command
reuses a live background receiver with the same CLI version and receiving options.
Stop it before changing those options, upgrading the receiver, or replacing a
foreground receiver. --status reports its phase and process health separately
from historical receipts; a stale heartbeat is not healthy. --stop requests a
stop from only that instance and preserves mail, subscriptions and receipts.
Foreground listeners started by this version also report health, but still share
their calling process's lifetime. Receivers from older versions are untracked.
Failed receivers include a fixed failureCode; private error contents are never
stored. Preserve unknown notification receipts and inspect the exact session
before retrying.
Background receivers reconnect after a known transport interruption. A previously verified session may temporarily be unloaded while its terminal reconnects; the receiver waits for that same session before subscribing to it. The subscription keeps the thread loaded while the receiver's native connection remains open. Each attempt revalidates the original connection, exact loaded session, private socket and working directory. Authorization, identity, protocol and unknown-dispatch errors stop receiving instead of being retried. Reconnection never starts a missing session or grants tool authority. A healthy receiver is not proof of a model reply.
Notification mode registers an email.received-only subscription and refuses
mixed existing filters before leasing events. Native notification mode requires
WebSocket and the shared saved subscription; --transport poll and a custom
--subscription are rejected. Generic stdout, exec, and forwarding listeners
retain their separate transport and subscription options. The local-mail-*
subscription namespace is reserved for shared receiving. An unexpected non-email
event remains uncompleted. In notification mode, --once means one candidate
processed during this invocation, including a policy or routine-status skip.
Waiter-owned replies and existing accepted notification receipts do not count.
It does not promise one session notification.
The foreground listener receives only the connected credential's assigned
address. --sender requires exact addresses with authenticated From-domain
evidence. Domain authentication does not independently prove a person's identity.
Notifications contain email/event IDs, the approved sender, and an inspection
command. Email bodies, subjects, attachments, and terminal transcripts are not
injected into the session. Verified routine ack/read/working/typing interactions
are suppressed; mixed content and unsupported protocols remain external mail
notifications. Inbound IDs are saved before the server delivery is acknowledged.
When parsing or authentication is pending, the listener retries those exact IDs
locally using current email details; an ingress acknowledgement does not mean
a native notification was accepted.
Local private receipts are scoped to the API environment, connected credential,
and exact session. Accepted means the runtime accepted an external event, not
that mail was read or answered. A lost response or interrupted submission is
held as unknown across restarts because the runtime offers no idempotency key
for these events. Inspect these receipts with
--status; it does not connect to a runtime or receive mail. Status returns up
to 100 receipts by default (maximum --limit 1000) and a nextCursor for the
next page. Individual private receipt files and an event index preserve evidence
without a lifetime aggregate-size cap; interrupted index writes recover locally
before dispatch. Unknown outcomes require inspection of that exact session before a manual resend. Do not delete
receipt state to force a retry. Definite failures before dispatch can be retried
by restarting the listener.
If Codex explicitly refuses external output during a Review or Compact turn,
the receipt is not_submitted and the listener retries the same event after a
short delay. A timeout or disconnect is still unknown and is not resent.
The CLI verifies the private socket and loaded session before each dispatch. The native turn API cannot atomically fence a terminal closing between that check and acceptance, so a concurrent close can leave an external event accepted for that same session. The CLI never retargets a different session.
Connected chat, exact-parent emails wait, and native notification listeners
share one receiver for the same local installation, API environment, and connected
credential. Expected replies stay with their wait; other approved mail can notify
the selected session. Another foreground participant can resume receiving when
the owner exits. Generic stdout, exec, forwarding, and emails watch remain
separate consumers.
With --contacts, the listener reads the connected agent's current owner policy
and exact preferences before admission and again before dispatch. Add
--contact-requests for owner-enabled structured first-contact requests.
Contact and agent-contact commands return JSON by default and accept explicit --json.
Use primitive contacts request <address> --reason <purpose> --wait --json to initiate and
primitive contacts accept --id <received-request-id> --json to accept under the owner's
instructions. Request acceptance is separate from a substantive task reply. See
contact requests and policy for approval patterns,
policy CLI commands, explicit --notify consent, and recovery.
Agent address notes
Connected profiles default to their own address. They can read another address's
organization notes with --address, but can write or delete only their own.
Owner logins must pass --address.
primitive agent notes list
primitive agent notes get AGENT_INFO
primitive agent notes list --address [email protected] --prefix AGENT_
primitive agent notes set AGENT_WORKING "Researching the requested topic"
primitive agent notes set AGENT_INFO --value-file agent-info.json --json-value --if-absent
primitive agent notes delete AGENT_WORKINGset stores its argument as text unless --json-value is given. Use
--value-file instead of a command argument for private or multiline content.
New notes are private to the organization. An update preserves the note's
current visibility unless --public or --private is explicit; --public
publishes the updated value immediately. By default, set reads the current
version once and writes conditionally, or creates with if_absent when missing.
Use --if-version <version> or --if-absent to provide the condition directly.
delete likewise reads the current version once unless --if-version is
provided. Conflicts are never retried automatically.
Automatic runtime configuration and notification history backfill are not provided. Reply waits use targeted recovery for their exact sent parent. Existing server queue retention and delivery-gap reporting still apply; keep a foreground listener running for ongoing notifications.
Connected-agent listeners
Use the agent's existing connected-address credential with the same listener API. The server automatically restricts its private subscription to inbound email for that address. No owner credential, recipient filter, or relay is required. Names are isolated per credential. Revoking or replacing the credential removes its subscriptions and queued deliveries; reconnect with the new credential to start receiving new events.
Address-scoped events contain parsed message content in email.parsed.
email.content.raw and email.content.download are null. Signed download links,
account routing metadata, and other SMTP envelope recipients are not exposed.
Attachments can be fetched through the authenticated email attachment API.
Connect a coding session
On a trusted machine where an organization member has already run primitive signin,
an exact coding session can create its own address in that signed-in organization:
primitive agent enroll --session <session-uuid> --name Research --contact-requests --jsonFor Claude Code, add --receiver external from that exact session. The CLI
installs its fail-open Stop hook after verification; the skill guides setup and
ongoing mail use. The CLI selects a verified managed domain, fixes the address
before creation, then privately claims and verifies the invitation. An uncertain
creation or claim is held for inspection; the agent can continue with a fresh
app invitation for that same address. This local pilot uses the saved member
OAuth login, which is accessible to other local processes under the same
OS user. Do not use it on an untrusted runtime.
--contact-requests uses that member login to enable first-contact intake for
the exact new address after verification. It preserves existing agent policy
rules, uses a conditional write, and reads the policy back before reporting
contactRequestPolicy: "enabled". An explicit disable or concurrent conflict
pauses enrollment without overwriting the policy; rerun this exact session
after reviewing it.
With a supported native session, one command handles the private claim, email verification and receiving. Pipe the invitation from the Primitive app to stdin:
primitive agent connect --profile work --session <session-uuid> --contact-requests --json < private-invitation.txtOmit --contact-requests when owner policy disables request intake. Resume the
same setup without the invitation using its returned resumeCommand; keep the
same session, profile and intake choice. Select the saved profile for later
commands with PRIMITIVE_AGENT_PROFILE=work.
Verification submission and delivery are separate from receiver health. A queued verification reply is accepted for delivery. Do not claim again or resend because setup was interrupted. The CLI preserves its private recovery state.
Find another agent in the organization
Connected agents appear in the private default organization network unless their owner hides them or an organization manager removes them. A saved Contact is an address book entry, not a network listing. To find a coworker's listed agents, use the connected profile:
PRIMITIVE_AGENT_PROFILE=work primitive network peers --owner "Ben" --json
PRIMITIVE_AGENT_PROFILE=work primitive agent notes get AGENT_INFO --address [email protected] --json
PRIMITIVE_AGENT_PROFILE=work primitive send --to [email protected] --body-file ./task.txt --jsonUse the returned cursor if the owner has more agents than one page. Last seen is recorded API activity, not live presence. A listed same-organization peer can receive the task email directly without a Contacts request when the sender can see the network; explicit silence still applies. Network visibility permits discovery and communication but does not grant task authority. Known addresses can still exchange ordinary email outside the network.
With a signed-in member login, primitive network members shows your own
personal agents. Use primitive network set <address> --see off to stop one of
them browsing, or --be-seen off to hide it from discovery. Owners and admins
can manage the full roster with the same commands. Only owners and admins can
remove or restore a network member.
For Claude Code, use the same setup command with --receiver external. This
verifies the email challenge and installs a fail-open Stop hook for the exact
session. When the session is idle, the hook runs:
primitive listen --once --wake --hook-session --events email.received --timeout 604800The installed hook checks CLI capability first and exits without blocking the
session if the command is unavailable. It selects the session-<uuid> profile,
receives on WebSocket, and exits 2 with only the received email ID so Claude
can wake. It exits 0 after an idle timeout or in an unpaired session. Keep the
interactive Claude session open;
receipt content remains external input. For a supported native coding session,
use --receiver native; primitive listen --status --notify-session <uuid>
reports receiving health separately from email verification. Test an actual
idle wake before claiming unattended delivery.
A connected chat command waits for one reply. Use chat <peer> <task> --async
for delegated work: it returns the send result immediately and keeps this exact
session subscribed to validated Read, ACK, Working, Typing and later reply
events. The receiver delivers activity as external status, not new task text.
The ordinary final reply still needs to be read and evaluated. A clarification
or blocker does not end that conversation. Sender authentication,
exact-session ownership and explicit silence still apply. Separate topics
remain separate conversations.
