Agents
An AI coding agent or a script drives SoCLI through one-shot commands that print JSON and exit, and never through the full-screen reader. SoCLI turns that behavior on by itself when it recognizes an agent, and --json turns it on for any caller. This page describes how SoCLI detects an agent, what changes in agent mode, the JSON each command prints, how to chain one command's output into the next, how errors and exit codes work, and how an agent can sign in. Every sample below was captured from a --demo --json run, so the ids and URLs are fixtures and nothing was sent to a network.
This site also serves llms.txt and llms-full.txt, which hold every docs page as plain Markdown.
Agent detection
src/agent.rs decides whether a process is run by an agent. It checks, in this order:
AI_AGENT, with any non-empty value. The value becomes the agent's name.- One variable per known agent, in the order of the table below.
- The file
/opt/.devin, which marks Devin.
| Agent | Variables |
|---|---|
cursor-cli | CURSOR_AGENT, or CURSOR_EXTENSION_HOST_ROLE=agent-exec |
gemini | GEMINI_CLI |
codex | CODEX_SANDBOX, CODEX_CI, CODEX_THREAD_ID |
antigravity | ANTIGRAVITY_AGENT |
augment-cli | AUGMENT_AGENT |
opencode | OPENCODE_CLIENT |
claude | CLAUDECODE, CLAUDE_CODE |
github-copilot | COPILOT_MODEL, COPILOT_ALLOW_ALL, COPILOT_GITHUB_TOKEN |
CURSOR_TRACE_ID and REPL_ID are deliberately not on the list, because the terminals a person opens in Cursor and Replit carry them too, and treating those people as agents would take their reader away.
SOCLI_AGENT=1 forces agent mode and SOCLI_AGENT=0 turns it off, whatever the other variables say. Any other value, or no value, leaves detection on. A script that wants the same behavior without being detected can pass --json on every call.
What changes in agent mode
Agent mode makes SoCLI safe to run with nobody at the terminal:
- Output is JSON by default.
--printasks for the text form instead, and--print --no-promptleaves out theSoCLI>prompt. - SoCLI never opens the reader, never prompts and never reads a key. A command that would ask a question fails with a usage error instead.
- Text for
post,replyandquotehas to be given as an argument, or as-to read standard input. An empty command line does not read standard input in agent mode. deleteneeds--yes, because there is nobody to answery/N.- Errors print as one JSON line on stderr (see Errors and exit codes).
- Output carries no color, no OSC 8 hyperlinks and no Nerd Font glyphs, and
--printuses Unicode icons. - The sign-ins that need a browser refuse to run (see Signing in).
Without a command, SOCLI_AGENT=1 socli prints the home feed as JSON, which is the same as socli feed --json.
One-shot commands
socli help lists every command, socli help <command> shows the flags and examples of one, and socli help --json prints the whole table as JSON, so an agent can read the current surface instead of this page. The commands an agent uses most:
| Command | What it does |
|---|---|
socli feed [following|latest|trending|discover|saved|mine|notifications] | The home timeline, or one named feed on the networks that have it |
socli thread <id> | A post with its replies |
socli search <query> | Recent public search on the networks that can search |
socli user <handle> | Public posts from one account |
socli notifications, socli bookmarks | Mentions, and saved posts |
socli post <text>, reply <id> <text>, quote <id> <text> | Publish, reply, quote |
socli like, repost, bookmark, follow <id or handle> and their un… forms | Toggle one state |
socli delete <id> --yes | Delete one of your posts |
socli whoami, socli auth status | The accounts in use and what is configured |
socli config list|get|set|path|themes, socli drafts [rm <n>] | Settings and saved drafts |
--net takes x, bluesky, devnull, threads, mastodon (or masto), a comma list or all. Without it, SOCLI_NET, then feed.networks, then every network applies, and post uses the network set by socli auth default. A reply goes only to the parent post's network. --count <n> sets how many posts each network is asked for (15 by default; the demo ignores it and always returns its whole feed), and each X post returned is billed, so an agent should not page on X without a reason.
Scoped ids
Every post id in JSON has the form <network>:<native id>, such as x:1849213377, bluesky:at://…, devnull:p2 or mastodon:115000000000000001. Pass that string back to thread, reply, quote, like, repost, bookmark and delete. A bare native id needs --net to say which network it belongs to, and a post link is accepted in place of an id. To chain commands, read id from one command's JSON and pass it to the next:
ID=$(socli --json feed --net bluesky | jq -r '.[0].id')
socli --json thread "$ID"
socli --json reply "$ID" "agreed" --dry-runThe ids in the first command's output are scoped already, so the second and third commands need no --net.
JSON shapes
Posts
Reads (feed, thread, search, user, notifications, bookmarks) print one array of posts, built by Post::to_json in src/model.rs. This is one post from socli --demo --json feed:
{
"author": {
"avatar": "demo://avatar/nuno",
"description": "Shipping the reader.",
"id": "devnull:u2",
"name": "Nuno Park",
"username": "nuno",
"verified": true
},
"bookmarked": false,
"conversation_id": "devnull:c9",
"created_at": "2026-10-20T18:37:47.585019+00:00",
"id": "devnull:p2",
"in_reply_to": null,
"liked": false,
"links": [
{
"display": "example.com/socli",
"expanded": "https://example.com/socli",
"image": "demo://og-wide",
"short": "https://example.com/socli",
"site": null
}
],
"media": [],
"metrics": {
"bookmarks": 0,
"likes": 86,
"quotes": 0,
"replies": 5,
"reposts": 0,
"views": 0
},
"network": "devnull",
"permalink": "https://dev-null.sh/@nuno/commits/p2",
"quoted": null,
"reposted": false,
"reposted_by": null,
"text": "ok devs, assemble. the reader is up.\n\nnotes: https://example.com/socli\n\nmake some noise.",
"url": "https://dev-null.sh/@nuno/commits/p2"
}The fields mean the following.
| Field | Meaning |
|---|---|
id, network | The scoped id and the network slug |
url, permalink | The post's link on its network (both hold the same value) |
conversation_id, in_reply_to | The thread the post belongs to, and its parent's scoped id or null |
reposted_by | The handle that reposted it into your timeline, or null |
created_at | An RFC 3339 time, or null |
text | The post text |
author | id, name, username, verified, avatar (a URL, or demo://avatar/<user> in the demo) and description |
metrics | replies, reposts, likes, quotes, bookmarks and views as numbers; a network without a count reports 0 |
liked, bookmarked, reposted | Whether the signed-in account did it |
links | short, expanded, display, and image and site when the network or the link page gave a card |
media | key, type, url, preview, alt, width and height per picture |
quoted | The quoted post as the same object, or null |
cid | Bluesky only: the record's content id, omitted on other networks |
Key order is alphabetical and is not part of the contract.
Write results
post, reply, quote, like, repost, bookmark, follow, delete and their undo forms print one array with one object per network that was addressed:
[
{
"network": "bluesky",
"ok": true,
"id": "bluesky:local-17",
"url": "https://bsky.app/profile/you.bsky.social/post/local-17",
"demo": true,
"text": "ship it"
}
]network and ok are always present. id and url appear when the network returned them, handle names the account of a follow or unfollow, text is the text a demo or dry-run post would send, and demo is true when nothing was sent because of --demo. When a network refuses, its object has "ok": false and an error line, and the process exits 1:
[
{
"network": "threads",
"ok": false,
"error": "Threads has no likes in its API"
}
]Dry runs
--dry-run checks a write against the sign-in, the network's capabilities and its length limit without sending anything. A passing post reports the length counted the network's way and the limit, and a failing one explains the failure:
[
{
"network": "x",
"ok": true,
"demo": true,
"dry_run": true,
"text": "ship it",
"length": 7,
"limit": 280
}
][
{
"network": "bluesky",
"ok": false,
"dry_run": true,
"error": "too long for Bluesky: 301/300"
}
]Accounts and configuration
socli whoami prints one object per network. handle and url are present only for a signed-in network:
[
{ "network": "x", "signed_in": false },
{ "network": "bluesky", "signed_in": false },
{ "network": "devnull", "signed_in": false },
{ "network": "threads", "signed_in": false },
{ "network": "mastodon", "signed_in": false }
]socli auth status prints the config path, the default network (null when unset), the image mode and one entry per network. A signed-in network adds handle, method (oauth, bearer, app password, device key, ssh key or token) and a detail string such as the Bluesky or Mastodon server. Secrets never appear.
{
"config": "/path/to/config.toml",
"default_network": null,
"images": "auto",
"networks": [
{ "network": "x", "signed_in": false },
{ "network": "bluesky", "signed_in": false },
{ "network": "devnull", "signed_in": false },
{ "network": "threads", "signed_in": false },
{ "network": "mastodon", "signed_in": false }
]
}socli config list prints one object with every key and its value (or default) plus path, config get <key> prints {"key", "value", "default"} where default says whether the value is the default, and config set <key> <value> prints {"key", "ok", "value"} with the value as saved:
{
"default": true,
"key": "ui.theme",
"value": "auto"
}socli config themes prints {"themes": [{"name", "description", "variants"}]} for every value ui.theme accepts, where variants is ["dark"], ["dark", "light"] or empty for auto, native, black and white.
socli drafts prints an array of drafts, empty when there are none ([]). Each draft has index (the number socli drafts rm <index> takes), id, targets, text, created_at, updated_at, and reply_to or quote when the draft is a reply or a quote. drafts rm prints {"ok": true, "removed": <index>, "id": "…"}. The demo run has no drafts, so these two shapes come from src/main.rs and not from a captured sample.
Sign-in and sign-out commands print one object per account: socli login <net> prints {"ok", "network", "handle", "url"} (handle and url when known), and socli logout <net|all> prints an array of {"network", "ok", "error"} with error only on a failure. Those two shapes also come from src/main.rs, because running them would change a real configuration.
The help table
socli help --json prints one object with name, summary, version, commands, global_flags, environment and exit_codes. Each entry of commands has name, summary, usage, flags (each with flag, value and description) and examples. exit_codes maps the three codes to their meaning:
{
"0": "success",
"1": "a network or target failed",
"2": "a usage error, or a step that needs a person at a terminal"
}Errors and exit codes
When errors are JSON (agent mode, or --json), a failure prints exactly one line on stderr and nothing on stdout:
{"error":"delete needs --yes when socli runs without a terminal"}Usage errors from the command line parser add usage, and tip when the parser has one:
{"error":"unrecognized subcommand 'frobnicate'","usage":"socli [OPTIONS] [COMMAND]"}Outside JSON mode the same message prints as socli: <message>. A failed write is the exception: it prints its result array on stdout and exits 1 without an extra stderr line. A closed pipe exits 0.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | A network or target failed: sign-in missing, a rate limit (<Network> rate limit reached; try again in N minutes, in Ns below two minutes, in a minute when the network sent no reset time), an unsupported feature, a post that does not exist |
| 2 | A usage error, or a step that needs a person at a terminal (a browser sign-in, a prompt, delete without --yes, socli help for an unknown command) |
Messages are lowercase and say what to run next, for example which network? add --net x|bluesky|devnull|threads|mastodon, or use a full id like bluesky:at://…, so an agent can show the message to the person or act on it.
Writing safely
Run every write with --dry-run first. The dry run reports the length and the limit on each network and fails for a network that is not signed in or lacks the feature, without sending anything. Show the person the text and the networks, then run the real command after an explicit confirmation. post, reply, quote, delete and follow are public or destructive, so an agent asks before each one. delete --yes skips SoCLI's own question and nothing else asks in its place, so the confirmation belongs to the agent and the person.
<b>, <i>, <strong> and <em> in post text become Unicode bold and italic letters. Screen readers spell those letters one by one, search does not match them, and on X each counts as two characters, so use --raw to send the text as typed.
socli share copies a link only when a person runs it in a terminal. In a pipe or in agent mode it copies nothing and prints the link, as {"url": "…", "copied": false} in JSON mode (and in agent mode) and as the bare link otherwise, with nothing on stderr and exit 0. It adds nothing for an agent, which reads url or permalink from the post JSON. socli ask reads posts that are already loaded and does not call a model.
Keep tokens out of command lines and logs. Prefer the environment variables below or standard input, and read socli auth status, which redacts every secret.
Signing in
An agent can sign in to three networks without a person:
| Network | Command |
|---|---|
| Bluesky | printf '%s\n' "$APP_PASSWORD" | socli login bluesky --handle you.bsky.social --password-stdin, or set BLUESKY_HANDLE and BLUESKY_APP_PASSWORD |
| Threads | socli login threads --token <token>, or set THREADS_ACCESS_TOKEN |
| Mastodon | socli login mastodon --token <token> --server <host>, or set MASTODON_ACCESS_TOKEN and MASTODON_SERVER |
X and dev/null need a person at a browser, and agent mode refuses their sign-ins with exit 2:
{"error":"signing in to X needs a person at a browser. Run socli login x in a terminal"}socli login devnull and socli auth threads oauth are refused the same way, and so is socli login mastodon without a token (a token comes from --token or MASTODON_ACCESS_TOKEN):
{"error":"signing in to Mastodon needs a person at a browser. Run socli login mastodon in a terminal"}Create the Mastodon token in the server's Preferences > Development with the scopes read write. integration/mastodon/auth.md lists both Mastodon sign-in paths. A person signs in once at a terminal, and the agent then uses the stored session. X also reads X_ACCESS_TOKEN, X_BEARER_TOKEN and the other X_* variables from the environment (listed in CONVENTIONS.md), and socli auth x token --access-token <token> stores a user token the person already has. The per-network pages in docs/integration/<network>/auth.md give every method.
Installing the skill
docs/agents/socli/SKILL.md is a ready-to-install Claude Code skill that teaches an agent these rules. Copy the folder docs/agents/socli/ to ~/.claude/skills/socli/, and Claude Code lists the socli skill in its next session.
Next: Quick start