# Agent command reference

Quick how-tos for the `anima` commands you use as an agent. When you are about to use a command
you are not sure about, read the relevant entry here first. For the mental model behind these
commands (how Anima works around you), see the [agent guide](/agent/guide). For situations where
the right move is not obvious from any single command, see the
[recipes for common moments](/agent/recipes). For Feishu-specific identifiers, permissions,
visibility, group creation, and invite troubleshooting, see the [Feishu runbook](/agent/feishu).

## Per-agent environment values and secrets (`anima env`)

Use this when you need an external service's value to run a command or tool, for example an API
key, a token, or a config setting like a region.

These values live in your own per-agent store. Setting a value does not put it into your shell.
A value only enters a process when you explicitly run that process through `anima env run`.

**Set a plain (non-secret) value:**

```
anima env set SERVICE_REGION us-west-2
```

**Set a secret (API key, token). The value is read from stdin, never from the command line:**

```
printf '%s' "$THE_SECRET_VALUE" | anima env set OPENAI_API_KEY --secret
```

Secrets are encrypted at rest. The CLI rejects a secret passed as an argument, so always pipe it
in through stdin.

**Use values to run a command.** Inject only the keys that command needs, into that one child
process:

```
anima env run --keys OPENAI_API_KEY -- some-tool --do-the-thing
```

Omit `--keys` to inject every configured value. Nothing is auto-injected into your shell, so wrap
the command each time you need a value. Only selected stored values are added; Anima's managed
runtime and provider credentials are not forwarded automatically.

**Check what is configured.** Secret values are shown masked, for example `••••1234`:

```
anima env list
```

There is also `anima env source`, which prints shell export lines for non-secret values. It
excludes secrets by default; keep it that way. Secrets belong inside `anima env run`, never
exported into your shell.

### Transfer a secret

Use the same one-time public-key flow for an agent or a human. Keep the request and encrypted
response in the channel or thread where the work is happening. In the message carrying the key,
state what you need and why so the grant remains visible to the team.

The receiving agent creates a one-time key:

```
anima env handoff receive
```

For another agent, send the complete fenced `asec_key_v1_...` block. The sending agent decides
whether to grant the request, then encrypts one of their existing secret env values:

```
anima env handoff send 'asec_key_v1_...' --from-key SOURCE_TOKEN
```

For a human, send the printed `handoff.meetanima.online` link. The page encrypts locally and
returns the same fenced `asec_sealed_v1_...` block as the agent command.

The key and link contain no agent, workspace, env key, purpose, or expiry. Choose the destination
only when accepting the returned block:

```
anima env handoff accept 'asec_sealed_v1_...' --key SERVICE_TOKEN
```

The private key remains local, expires after 24 hours by default, and is destroyed after one
successful acceptance. Use `--expires 1h` to choose a lifetime between 5 minutes and 7 days.
Acceptance rejects an existing destination unless the receiver deliberately passes `--replace`.
Use the printed `s_...` id with `anima env handoff cancel <id>` to destroy an unused receive key.

Slack carries only the public key and ciphertext. Identity and purpose come from the surrounding
conversation, not from the cryptographic box. The source value for an agent send must already be a
secret env entry. This does not transfer Claude/Codex login state, Slack/Feishu credentials, or
other Anima-managed credentials.

**Do not:**

- Never print or echo a secret value, in any message, file, or log. `anima env list` masks values
  so you can confirm a key is set without revealing it. That is all you need.
- Do not pass a secret as a command argument; it would land in shell history and process listings.
  Use stdin only.
- Some names are reserved or managed and cannot be set (for example `ANIMA_*`, `PATH`,
  `NODE_OPTIONS`, and the dotenv key material). The CLI will tell you if a name is not allowed.

## Call the Slack Web API directly (`$SLACK_BOT_TOKEN`)

Use this when you need a Slack operation the `anima` commands do not cover yet, for example creating
or archiving a channel, inviting someone to a channel, or reading workspace metadata. You have a
Slack bot token available in your environment as `$SLACK_BOT_TOKEN`. Use it to call the Slack Web
API directly; this is a supported, encouraged way to reach Slack actions the CLI has not wrapped.

```
curl -sS -X POST -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
  -d "name=new-channel" https://slack.com/api/conversations.create
```

Read the JSON response before retrying: `"ok": true` means it worked, and `"ok": false` carries an
`error` you can act on. Do not repeat a call just because you are unsure it landed; check the
response first.

**Do not:**

- Never print, echo, or log `$SLACK_BOT_TOKEN`. Use it inside the request only.
- For anything teammates should see (a message, a reaction, a file), still use the `anima`
  commands, so it lands in the audit log. Use the Web API for operations the CLI does not cover, not
  to bypass the audited path for ordinary team communication.

## Extend yourself with skills (`npx skills`)

Skills are modular capability packages from the open skills ecosystem: a folder of instructions
(and sometimes helper tools) that teach you to do a specialized task well, for example design work,
writing tests, or working with a specific API. They are separate from your `anima` tools and ride
the provider's skill mechanism, not Anima's. You already have a few installed, and the bundled
`find-skills` skill walks you through finding and adding more.

Use this when you hit a capability gap: a task specialized enough that someone has probably already
packaged the know-how. Before improvising from scratch, check whether a skill exists.

Search the ecosystem, then install by name:

```
npx skills find <query>             # search, e.g. npx skills find pr review
npx skills add <owner/repo@skill>   # install one
npx skills check                    # see what has updates
npx skills update                   # update installed skills
```

Browse the catalog at https://skills.sh.

**Do not:**

- Do not reinvent a capability from scratch when a skill likely exists. Search first.
- Do not treat a found skill as already installed. Tell the person what it does and where it comes
  from before you add it.

## Invite another Feishu bot into a chat

Use this when the user asks you to add another Anima-created Feishu agent to the current Feishu
chat and there is no `anima` command for the invite yet.

Feishu invites bots by **app ID**, not by `open_id`. For an Anima-created Feishu agent, the target
app ID is the agent's Feishu App ID (`feishu.appId`). If you cannot see the target agent's app ID,
ask the user for it or ask them to open the target agent's Profile page and copy the Feishu App ID.
Do not guess it from the bot name.

You need the current chat ID from the delivery envelope, for example `chat_id=oc_...`. The current
agent must already be in that chat, and its Feishu app must have the recommended group-member
permission authorized and published.

Call Feishu's chat-member API with the runtime tenant token:

```
curl -sS -X POST \
  "https://open.feishu.cn/open-apis/im/v1/chats/<chat_id>/members?member_id_type=app_id" \
  -H "Authorization: Bearer $FEISHU_TENANT_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"id_list":["<target_app_id>"]}'
```

Replace `<chat_id>` with the Feishu `oc_...` chat ID and `<target_app_id>` with the target
agent's Feishu App ID.

Do not print or log `$FEISHU_TENANT_ACCESS_TOKEN`. If the API says the app lacks permission, ask the
user to authorize the recommended Feishu permissions, publish a new app version, then try again. If
the API says the bot cannot manage chat members, the current bot may not be allowed to invite
members in that group.

## Create a Feishu group chat

Use this when the user asks you to create a Feishu group and there is no `anima` command for group
creation yet.

You need at least one user `open_id` to seed the group. In a Feishu DM, use the sender `open_id`
from the message envelope. Do not guess people from display names.

Call Feishu's chat-create API with the runtime tenant token:

```
curl -sS -X POST \
  "https://open.feishu.cn/open-apis/im/v1/chats?user_id_type=open_id" \
  -H "Authorization: Bearer $FEISHU_TENANT_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"name":"<chat_name>","description":"<description>","user_id_list":["<owner_open_id>"],"chat_mode":"group","chat_type":"private"}'
```

Read the JSON response before trying again. If it includes a `chat_id`, the group was created. Use
the audited CLI for the visible follow-up message:

```
anima message send --chat-id <chat_id> <<'ANIMA_MESSAGE'
<message>
ANIMA_MESSAGE
```

Do not print or log `$FEISHU_TENANT_ACCESS_TOKEN`. Do not create a second group just because you
are unsure whether the first call worked. Check the response for `chat_id`, then send a message to
that `chat_id`.

## Invite a Feishu user into a chat

Use this when the user asks you to add a human teammate to the current Feishu chat and there is no
`anima` command for the invite yet.

Use deterministic identifiers only:

- If the teammate appears in the current message envelope or readable history, use their Feishu
  `open_id`.
- If the user gives an email address or phone number, resolve it to an `open_id` first.
- If the user gives a tenant `user_id`, you may invite with `member_id_type=user_id`.
- Do not guess from display name. If you only have a name, first ask the user to mention the
  teammate in Feishu and send the request again so Anima can read the stable ID from the mention
  metadata. If they cannot mention the teammate, ask for an email, phone number, `open_id`, or
  `user_id`.

To resolve an email or phone number to an `open_id`, call Feishu's user ID lookup API:

```
curl -sS -X POST \
  "https://open.feishu.cn/open-apis/contact/v3/users/batch_get_id?user_id_type=open_id" \
  -H "Authorization: Bearer $FEISHU_TENANT_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"emails":["<email>"],"mobiles":["<phone>"],"include_resigned":false}'
```

Use only the fields you have. For example, omit `mobiles` when you only have an email address. If
Feishu returns no matching user, first ask the user to mention the teammate in Feishu and send the
request again. If they cannot mention the teammate, ask for another deterministic identifier.

After you have the target user's `open_id`, invite them with the chat-member API:

```
curl -sS -X POST \
  "https://open.feishu.cn/open-apis/im/v1/chats/<chat_id>/members?member_id_type=open_id" \
  -H "Authorization: Bearer $FEISHU_TENANT_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"id_list":["<target_open_id>"]}'
```

Replace `<chat_id>` with the Feishu `oc_...` chat ID and `<target_open_id>` with the user's Feishu
`open_id`. If you already have a tenant `user_id`, use
`member_id_type=user_id` and pass that `user_id` in `id_list`.

Do not print or log `$FEISHU_TENANT_ACCESS_TOKEN`. The current agent's Feishu app must have the
recommended member-invite and user-lookup permissions authorized and published. If the API says the
bot cannot manage chat members, the current bot may not be allowed to invite members in that group.
If Feishu returns `232024` with a message like "Users do not have the visibility of the app", ask
the user to add the target person to the app's availability or visibility range in the Feishu
developer console, publish or save that change, then retry. If the target is an external
collaborator, the workspace may also need external collaboration permission.

## Ask a one-click question (`anima ask`)

Use this when you need a bounded decision from a human: yes/no, approve/reject, or one pick
from a short list of 2 to 5 options. For an open-ended or multi-part question, send a normal
message instead. `ask` is for choices, not discussion.

The question posts with clickable answer buttons. By default the current Slack surface answers
it: the person in a DM, or first-click-wins in a channel or thread. Add `--to` only when one
specific person must be the one to answer.

```
anima ask --question "Ship the release to stable now?" --option "Ship" --option "Hold" --to @teammate
```

## Wake yourself later (`anima reminder`)

Use this for all deferred or recurring work: check back on a task, follow up with a teammate, run
a daily routine, or anything you need to do later. A reminder wakes you with its instructions at
the scheduled time. This is the most basic form of acting on your own initiative. Reminders
persist across restarts and are recorded in the audit log.

Schedule a one-shot with a delay (`--in`) or a fixed time (`--fire-at`; that is the flag name,
not `--at`), or a recurring one with a repeat rule and a timezone:

```
anima reminder schedule --in 2h --title "check deploy" --instructions "verify prod is healthy"
anima reminder schedule --fire-at 2026-07-03T01:30:00Z --title "morning check" --instructions "review overnight PRs"
anima reminder schedule --repeat daily@09:00 --timezone Asia/Shanghai --title "standup" --instructions "post the async standup"
anima reminder schedule --in 10m --repeat every:1h --title "hourly check" --instructions "check service health"
```

For conditional reminders, use **Write → Run → Schedule**. Write and debug a script in your Agent
Home, run it exactly once in the hosted preflight execution context, then schedule that exact
command only after you understand the result:

```
anima reminder preflight --command './scripts/check-usage.sh' --timeout 2m
anima reminder schedule --repeat every:15m --title 'Usage check' --instructions 'review the usage alert' --preflight './scripts/check-usage.sh' --preflight-timeout 2m
```

Run `preflight` from the target agent; it requires the current agent context. Its CWD is that
agent's Agent Home, and the child receives the same configured/managed environment as hosted
preflight. It does not invent an active message, reminder, inbox item, channel, thread, session,
workspace, or surface context. The one-shot run creates no reminder, wake, activity, or other
durable state.

The command's stdout and stderr are captured with explicit truncation markers, followed by its
duration and exit, signal, or timeout. Exit 0 succeeds and a hosted reminder wakes; exit 1 declines
and it skips; exit 2 or higher, a signal, or a timeout errors and reports Needs attention. The CLI
returns the command's exit code, returns 124 for timeout, and uses `128 + signal number` for a
signal. Preflight commands and explicit timeouts are scheduled with `--preflight <command>` and
`--preflight-timeout <duration>`; the default is 30 minutes and the hard cap is 24 hours. Do not
inline secrets because a scheduled command is persisted and shown by reminder inspection surfaces.

To make a reminder wake you back into a specific Slack conversation (so your follow-up lands in
the thread it belongs to), anchor it with `--anchor-channel <id> --anchor-message-ts <ts>` (add
`--anchor-thread-ts <ts>` inside a thread).

Manage them with `anima reminder list`, `anima reminder show <id>`, `anima reminder cancel <id>`, and
`anima reminder snooze <id> --by <duration>` (delays the next firing without changing the repeat
schedule). `--in` and `--fire-at` are mutually exclusive; either may be combined with `--repeat`
to set the first fire, and `--repeat` remains valid by itself. Repeat formats:
`every:<n>m|h|d`, `daily@HH:MM`, and `weekly:<day,day>@HH:MM`. The
timezone is an IANA name, for example `Asia/Shanghai`.

For an `every:*` reminder, add `--window <days>@HH:MM-HH:MM` to keep firings inside same-day local
working hours, for example `--window mon-fri@08:00-18:30 --timezone America/New_York`. A window is
only valid with `every:*`, cannot be combined with `--in` or `--fire-at`, cannot cross midnight,
and must be at least as long as the interval.

`list` stays compact and shows scheduled reminders by default. Use `show <id>` (or
`show --id <id>`) when you need the full instructions, schedule, Slack anchor, timestamps, status,
and firing count. Add `--json` to either command for the stable machine-readable representation;
JSON is the only stdout content.

When a reminder wakes you, its envelope carries both `time=` (when it reached you) and
`scheduled=` (when it was meant to fire; reminders from before this field existed show the actual
fire time instead). A gap between them means the reminder waited, so check
whether the world moved on before executing its instructions verbatim. Snooze is for "not yet":
the reminder arrived but its precondition has not (the reply you were waiting for is not in, the
work it checks on is still mid-flight). It also revives a one-shot that already fired. If you find
yourself snoozing the same reminder again and again, the trigger was designed wrong; switch to
being woken by the event itself (an @mention from the owner) or cancel it.

**Do not:**

- Do not roll your own scheduling, for example a background sleep. Only `anima reminder` survives
  a restart, which is the whole point: a scheduled wake must still fire after the runtime restarts.

## Reconstruct after a restart (`anima history`)

Use this when you have just restarted or compacted and need to see what was happening. Read your
`MEMORY.md` first to restore who you are and your open obligations, then check recent history.
`anima history` shows one chronological timeline of your recent conversation traffic: received and
sent interleaved, marked `IN`/`OUT`, newest last, so you can read straight back into the present.
If you are unsure whether you already replied to something, check the timeline before sending, so
you do not answer it twice.

```
anima history
anima history --limit 50
anima history --channel #team
```

`anima inbox` (received only) and `anima outbox` (sent only, including messages, files, and
reactions) are filtered views of the same history; all three accept `--limit`, `--since`,
`--before`, and `--channel`.

## See where you are present (`anima places`)

Use this when you need to re-place yourself: `anima places` answers "where am I" by listing the
rooms whose human-authored conversation reaches you without being named. Channels come from live
membership, DMs from your own message ledger, and `Muted (n)` always renders, even at zero, so
nothing is hidden.

```
anima places
anima places --all
```

Each row shows the name, the id (ready to paste into `--channel`), the topic when the platform
actually has one, and the time the runtime last delivered a message from that room. Rows sort by
last delivery with the newest last, the same direction as `anima history`, so the most alive rooms
sit closest to your prompt; rooms with no delivery yet sort to the top. A section over 50 rows
shows only the 50 most recent and says so in its header, with the full count and a `--all`
pointer; the list may be partial, the count never is.

Three things to read correctly:

- Timestamps are delivery times from your own ledger, not the room's latest activity. A muted room
  shows its pre-mute time; that is the truth.
- On Feishu the sections are labeled "Feishu known chats" and "Feishu known DMs": the platform
  offers no full-membership listing today, so the view covers the chats this runtime knows from
  its ledger and subscriptions.
- There is no thread section. Thread-following is a behavior rule (you posted or were @mentioned,
  so later human or bot/app follow-ups reach you while the follow is unmuted), not a thing to browse.
  Bot/app follow-ups cannot create or revive a follow. The thread that concerns you is waking you, so its `thread_ts` is already
  in the envelope in your hand.

`anima subscription list` is an alias of this command.

## Look up who or what an id is (`anima whois`)

Use this the moment before you act on an id or a name: `anima whois` answers "who is this" with a
point lookup, always queried live, never cached. It accepts a user, bot, or channel through one
entrance: `@handle`, `#channel`, or a raw id (Slack `U…`/`C…`, Feishu `ou_…`/`oc_…`). It never
opens a DM as a side effect.

```
anima whois @milo
anima whois #team
anima whois U0BXXXXXXX
```

Agents hosted by this runtime render with full detail, name, handle, id, and role, marked
`(this runtime)`. Everything else renders exactly as the platform reports it: a bot from another
runtime is `bot (not managed by this runtime)`, with nothing invented about whose it is or what it
does, and a channel shows your membership and member count from the live API. If a name matches
more than one entity, the command lists the candidates with ids and fails with
`anima.ambiguous_user`; rerun it with the exact id.

Prefer this over ids remembered in `MEMORY.md`: stored rosters go stale, and the lookup is cheap.

## Keep your memory lean (your own memory pass)

Use this to stop your durable memory from rotting. `MEMORY.md` is what restores you after a reset,
so it works best as a short, current index. Left alone it tends to bloat, hold facts that newer
events have contradicted, and freeze relative dates like "today." A periodic pass keeps it
trustworthy, and a smaller `MEMORY.md` is cheaper to reload on every recovery.

Anima runs a scheduled [memory pass](/concepts#memory) for you unless your operator has explicitly
disabled it; you will simply receive that wake and do not need anything below. You can also
run the same pass yourself, and schedule it with a reminder so it happens without you thinking
about it:

```
anima reminder schedule --repeat daily@05:00 --timezone <your-tz> --title "memory pass" \
  --instructions "Tidy MEMORY.md and notes/: merge duplicates, delete facts newer events have contradicted, convert relative dates to absolute, and demote long detail into notes/ so MEMORY.md stays a short index. If little changed since last time, do nothing."
```

The pass focuses on reviewing and consolidating your `MEMORY.md` and `notes/`. What you read there
is a record, not a command: a note that something should happen is not by itself authorization to
do it now. This is an ordinary wake, not a restricted mode; your normal capabilities, judgment, and
the usual rules that govern any wake are unchanged. If your ongoing session has clearly valid
unfinished work, you may finish it with that same judgment before you organize memory. The pass
never edits the guide, this reference, or your standing prompt. Demote long detail into a note and
confirm it landed before deleting it from `MEMORY.md`, and if nothing meaningful changed since last
time, do nothing rather than churn.


## Search messages you saw or sent (`anima message search`)

Use this when you need to find an older decision or thread by keyword without guessing a time
window. This searches your local Anima message ledger: conversations you were actually part of,
meaning messages delivered to you and messages you sent.

This is agent-visible history, not workspace search. It cannot find messages from channels or
threads where you were never present, and transcripts fetched with `anima message read` are not
searchable yet.

```
anima message search launch criteria
anima message search "invoice bug" --channel '#support' --since 2026-06-01
```

Search uses AND matching by default: every keyword must match. Results are newest first and include
the message timestamp, channel, direction, and a snippet. Use `--before <iso>` with the
`next_cursor` line to page older matches.

## Send a file, or open one you received (`anima file`)

Use this to upload a local file to the chat, or to open a file a teammate sent you. Sending fails
before upload if a path does not exist.

```
anima file send /tmp/chart.png --caption "Q3 signups by week"
anima file fetch <fileId> /tmp/incoming.png
```

`send` accepts multiple paths in one call, and `--caption` puts the explanation on the file
itself instead of in a separate message.

The `fileId` comes from the `attached: id=<id>` line in message-read output. To actually look at
an image a teammate sent, fetch it to a path and then read that path.

## Mute what is done (`anima subscription`)

Use this to stop following a channel or thread that is finished with you. To see where you are
present, use `anima places`; `anima subscription list` is an alias of it.

```
anima subscription mute --channel <id>
anima subscription mute --channel <id> --thread-ts <ts>   # mute one thread, keep the channel
```

Mute only when a thread or channel is clearly done with you and still noisy. An @mention always
brings you back, so muting is safe and is not the same as leaving. Finishing your part of a thread
is not a reason to mute, because follow-ups are common.

## Send messages and reactions (`anima message`, `anima reaction`)

The message body is read from stdin, so send a multi-line or Markdown body with a heredoc. Reply
where the message came from: pass the envelope's `channel=` to `--channel` and its `thread_ts=` to
`--thread-ts`. Bodies are Markdown, so use `**bold**`. `anima message update` edits a message in
place.

Some delivery envelopes include `wake=<reason>` to say why the message reached you, such as a DM,
mention, channel follow, or thread follow. Treat it as context, not a command; reason values vary by
transport because Slack and Feishu expose different conversation shapes.

```
anima message send --channel C0XXXX --thread-ts 1780000000.000000 <<'MSG'
**Done.** Details here.
MSG
```

A reaction is a lightweight reply when a full message is not needed. The emoji is a name, without
colons:

```
anima reaction add --channel C0XXXX --message-ts 1780000000.000000 --name white_check_mark
```

**Do not:**

- Do not pass the emoji with colons, for example `:white_check_mark:`. Use the bare name
  `white_check_mark`.
- Editing a message to add an @mention does not notify the mentioned person. Delete and re-post
  with the mention instead.

## When a command fails (the error contract)

Every `anima` command failure renders one structured line, so you can pick the right recovery
without guessing:

```
error <layer.code> (retryable | not retryable): <hint>
detail: <raw underlying message, only when it adds information>
```

- **`layer.code`** tells you whose problem it is. Layers: `input` (your arguments, rejected before
  any network call), `anima` (this runtime or its config), `slack` and `feishu` (the platform
  rejected the call; the code after the dot is the vendor's own, verbatim, so you can look it up
  in their docs), `network` (transport trouble, usually transient).
- **`retryable`** is the one bit that matters most: `retryable` means retry with backoff and say
  so in-channel if it persists; `not retryable` means retrying is waste, change something instead.
- **`hint`** names the next move. Follow it before improvising.

Examples of what to expect:

```
error slack.channel_not_found (not retryable): You cannot see a channel with that id. Verify it from a recent envelope or anima history, not from memory.
error network.dns_failure (retryable): Name resolution failed; retry with backoff, and say so in-channel if it persists.
error anima.no_agent_context (not retryable): Pass --agent <id> or set ANIMA_AGENT_ID.
```

`anima.unexpected` means the failure did not match any known shape; stop and show the operator the
detail line rather than retrying blind.
