lwis docs

Agents / Hosted agents

Hosted agents

A hosted agent is a headless agent that lwisd runs for you on demand: a receipts filer, a triage bot, a script. You send it a capture from your phone like to any session, and its answer comes back as a reply.

How a hosted agent runs

Each agent is a TOML profile in ~/.config/lwis/agents/<name>.toml (on macOS, ~/Library/Application Support/lwis/agents/). lwisd loads the profiles at start; after editing one, run lwis agents reload.

The full list of keys and rules is in Agent profile format.

Runtimes

RuntimeWhat lwisd runs
claude-sdkclaude -p in stream-json mode, with --restricted --safe-mode (no user or project settings, plugins, hooks, skills or MCP servers), your permission_mode, and only the tools you list. lwisd answers every permission prompt itself.
commandAny program, started in cwd (a leading ~/ is expanded), that speaks the lwis agent contract as JSON lines on stdin and stdout. It gets LWIS_AGENT=<name> in its environment.
webhookOne HTTP POST of the same contract message to a localhost URL, with the image bytes inlined as base64.

codex-exec is recognised but refused: it isn't implemented yet.

The agent contract

For command and webhook, lwisd sends one JSON object per capture:

{"type": "capture", "cid": "c_…", "agent": "receipts", "strict": true,
 "from": {"id": "…", "name": "Pixel", "self": true},
 "intent": "…", "note": "…", "gestures_text": "…", "untrusted": "<untrusted …>…</untrusted>",
 "text": "…the same prompt a claude-sdk agent gets…",
 "parts": [{"role": "annotated", "idx": 0, "mime": "image/png", "size": 123456, "path": "/run/user/…/…png"}]}

For a contact's capture (forwarded, or sent directly to an agent you allowed), from names the contact with your petname and their fingerprint, self is false, intent and note are null (their text moves inside untrusted, as data), strict is always true, and when you forwarded it, via is "self" and owner_note carries your own request (otherwise both are null).

A webhook gets data (base64) instead of path in each part. The agent answers with zero or more {"type": "reply", "cid": "c_…", "text": "…"} lines, then {"type": "done", "cid": "c_…"}. A webhook may instead return a JSON array of those objects, a single one, or {"text": "…"}. Lines for another capture id, or that don't parse, are ignored. A turn that doesn't finish within turn_timeout_seconds (default 300) fails.

Test a contract program without lwisd:

lwis agents run --image receipt.jpg --note "file this" -- ./my-agent.py

Manage agents

lwis agents list                 # loaded profiles, refused files, pending approvals (--json)
lwis agents check receipts.toml  # validate a file; for claude-sdk, print the exact claude command
lwis agents reload               # re-read the profiles

A profile that fails validation is refused, and lwis agents list shows why. Profiles and their directory must be owned by you and not writable by group or others.

Strict approvals

Profiles are strict by default (strict = true). In a strict claude-sdk agent, read-only tools (Read, Glob, Grep, LS, TodoWrite, NotebookRead) run straight away, and every other tool call waits for you:

  1. lwisd shows a desktop notification ("lwis: receipts wants to Write inbox/2026-09.csv") and sends the same notice to your phone as a plain-text reply.

  2. Review it:

    lwis agents list
    
  3. Approve or deny it by its approval id:

    lwis agents approve a_…
    lwis agents deny a_…
    

An action nobody approves is denied after approval_timeout_minutes (default 10). Approval ids are random and never shown to the agent; lwis accept <cid> never approves an agent action. A tool granted only with a specifier, such as Bash(git status), also waits for you, even when the agent isn't strict: lwisd doesn't evaluate the specifier.

Approving from the phone isn't available yet: you approve on the computer.

Who can reach an agent

accept_from lists who may send captures to the agent. The default is ["self"]: your own paired devices, and captures you forward.

A contact can reach an agent directly only when both hold:

Otherwise their capture stays in your inbox, and you decide whether to forward it.

Another machine's agent reaches a hosted agent through an agent pairing: name it as "agent:<label>" in accept_from. Its reports also run strict, in a fresh process, and its answer goes back to that agent.

Whatever the profile says, a contact's capture always runs strict, in a fresh one-shot process that never sees your earlier conversation with the agent. A permission mode other than default or plan is lowered to default for it.

Forward a contact's capture

Captures from other people land in lwis inbox, never directly in a session or agent. To hand one to an agent or a Claude session:

lwis forward c_… --to receipts --note "file this receipt"
lwis forwards      # what you forwarded, and the replies

See Inbox, replies and forwarding and Agent safety.