---
name: sharedoc
description: Read and write a share-doc workspace from the command line — workspaces, projects, files, revisions, the change log, threads (doc comments), channels, DMs, drafts and the sync set. Use the `sharedoc` CLI for every read and write; never edit a connected folder blindly; keep each project's sharedoc-instructions.md true after a pairing.
when_to_use: The user mentions share-doc, sharedoc, a share-doc project or workspace, a connected folder, or asks you to read, summarise, search, comment on, or edit a document that lives in share-doc.
---

# sharedoc — the share-doc CLI for agents

`sharedoc` is a first-class client: everything the web app can do, deterministic `--json`, and
non-interactive auth. Server: https://sharedoc.brightmotion.io. Install: `curl -fsSL https://sharedoc.brightmotion.io/install.sh | sh`;
update: `sharedoc update`.

**Always pass `--json`.** Every command then prints exactly one JSON document on stdout (errors
too) and exits with a meaningful code, so you never parse a table.

## 1. Get scoped

Before anything else, find out who you are and what you can see:

```bash
sharedoc whoami --json
```

That returns your user, each login (workspace + scope + token prefix), the projects you can open,
and the server it is talking to. Then narrow every later call:

```bash
sharedoc projects list --workspace justin --json
sharedoc files list --project homa-legal --json
```

- `--workspace <slug>` picks the workspace (needed when you have more than one login).
- `--project <slug>` picks the project; inside a connected folder it is inferred.
- Headless / CI: set `SHAREDOC_TOKEN` (and `SHAREDOC_HTTP` for a non-default host) instead of
  running `sharedoc login`. Env beats the config file.

If `whoami` says *not logged in*, log in for them — a person approves in the browser, but you can
run the login and send them the link:

```bash
sharedoc login --no-wait --json          # → { url, next } — send the human the url
sharedoc login --code <sd_code_…> --json # the code their browser shows, within 5 minutes
```

Tell them which workspace to tick and to pick *write*. Only ask them to run `sharedoc login`
themselves if you cannot do this. Never invent a token, and never print one you find. Setting a
machine up from scratch (install, project, folder, service) is a runbook of its own: https://sharedoc.brightmotion.io/setup.md.

### Global flags

```
--json — Print exactly one JSON document on stdout (errors too). Agents: always use this.
--workspace <string> — Workspace slug/id/name to act in (default: the connected folder, else your only workspace)
--project <string> — Project slug (default: the connected folder, else your only project in the workspace)
--host <string> — Server base URL (default: $SHAREDOC_HTTP, else the host you logged in to)
--as-agent <string> — Attribute writes to an agent, e.g. 'claude-code' (also $SHAREDOC_AGENT)
--as-model <string> — The model behind --as-agent, e.g. 'opus-5' — shown as 'via claude-code (opus-5)' (also $SHAREDOC_MODEL)
--limit <number> — Max rows for list commands (default 50, max 1000)
--cursor <string> — Continue a list from a previous nextCursor
--yes — Skip confirmation for destructive commands (agents are non-interactive)
--dry-run — Show what would happen; write nothing
--quiet — No footer / hints on stderr
--allow-cwd-env — Keep SHAREDOC_* that bun loaded from the working directory's .env (the installed CLI never loads it)
```

## 2. Read before you write

Writes are cheap to make and expensive to undo. Read first, every time:

```bash
sharedoc files list homa-legal --json            # what is in the project
sharedoc files cat homa-legal contract-v3.md     # the current text
sharedoc files history homa-legal contract-v3.md --json   # who changed it, when
sharedoc threads list --doc homa-legal/contract-v3.md --json   # what is already being discussed
sharedoc thread show <id> --json                 # the whole conversation before you add to it
sharedoc events tail --json                      # what happened while you were away
```

**Read the threads on a document before you comment on it.** Most of the time the point you are
about to make is already a thread, and the right move is a reply, not a new one. `threads list`
shows open threads by default (`--status all` includes resolved ones).

**In a connected folder the comments are on disk.** A doc with open threads has a read-only
`<doc>.comments.md` next to it: each thread's quote, the line it is on in that copy (`L41`, or
`L?` when the text moved or repeats), the messages, and a `thread:<id>` you can reply to. Read it
before you edit the doc. Never write a `*.comments.md` yourself — the agent regenerates it and the
server refuses the name as a document.

Then verify after you write: re-read the file (or the thread) and confirm your change landed
before you tell the human it did.

## 3. Commenting on a document

A comment is a **thread with a doc anchor**: quote the exact text you are reacting to, say one
thing, and tag what you are talking about.

```bash
sharedoc thread create --doc homa-legal/contract-v3.md \
  --quote "either party may terminate on 30 days' notice" \
  --body "This conflicts with the 90-day notice in [[homa-legal/term-sheet.md#L40-L48]]. @Justin which one wins?" \
  --json
```

That returns `{ thread, message, references, unresolved }`. The `--quote` has to be text that is
really in the document — copy it from `files cat`, don't retype it — because that is what anchors
the highlight.

### Reply, resolve, follow — the etiquette

```bash
sharedoc thread reply <id> --body "Agreed — 90 days it is." --json
sharedoc thread resolve <id> --json
sharedoc thread follow <id> --json
```

- **Reply in the existing thread** instead of opening a parallel one, and quote the part you are
  answering so the reply stands on its own.
- **Resolve only what you resolved.** A thread you answered but did not settle stays open; someone
  else's open question is never yours to close.
- **One thread per point.** Ten small threads on one document is noise; if you have ten remarks,
  write one thread that lists them, or put them in the document.
- **Reopen rather than re-create**: `thread reopen <id>` keeps the history in one place.
- **Say who you are acting for, and what you are.** `--as-agent claude-code --as-model opus-5`
  records the message as "Justin · via claude-code (opus-5)". Name the model you are actually
  running as — readers weigh a thread by it. Never post as if you were the human unattributed.

### Drafting for your human

When the human asks you to write a message, **draft it rather than post it**, unless they said
"post" or "send". A draft is private to them and waits, unsent, in their composer or on their
Drafts page; nothing notifies anyone until they send it.

```bash
sharedoc draft create general --body "Deployed v0.30 — notes in [[ops/deploys.md]]" --json
sharedoc draft create sharedoc --thread <id> --variants-file - --note "Colin asked when the deploy lands; B commits to a date" --json < versions.json
sharedoc draft set show <set-id> --wait --json      # exit 0 chosen · 4 dismissed/failed · 7 still open
sharedoc channel post general --body "…" --at "mon 9:00"   # schedule — only when asked for a time
```

- One body goes into the human's composer for that channel or thread. If they already have text
  there, it becomes a set of one next to it instead: **you never overwrite what they typed**.
- When the wording matters (a customer, a decision, a sensitive DM), offer **2–4 versions that
  differ in substance**, not in synonyms: short vs complete, committing vs hedging, with the numbers
  vs without. Label each version by that difference. `--variants-file` takes
  `[{ "label": "short", "body": "…" }, …]` (`-` for stdin). Say what you read and why in `--note`.
- **Never `draft pick` or `draft send` for the human**, and schedule only when asked. Tell them
  where the draft is waiting, with the set id. Use `--wait` only when you have a real follow-up to
  do once they have chosen.
- The outcome is yours to read: `draft set show` says which version won and whether they edited it.
  A version sent unchanged is posted as "them · via you"; an edited one is theirs alone.

### Slack conversations

Slack conversations your human connected are channels like any other: `channel history`, `thread show`
and `thread reply` work on them, and their chat-mirror folders start with `slack-`
(`~/sharedoc/chat/slack-<team>-<channel>/`). What is posted in a two-way one goes to Slack **as your
human**, so draft there unless they said to post; nothing posted in a read-only one reaches Slack.
`sharedoc slack status` shows each link's direction and health, `slack links` lists or changes them, and
`slack file <message-id>` fetches a file someone shared in Slack.

### Shared channels

Some channels are shared with other workspaces: `channels list --json` marks them with `shared`
(who else is in it), and messages from people on another side carry `authorWorkspace` ("Jason ·
Acme"). On the ShareDoc hub every workspace has one, `#talk-to-sharedoc`, where your human's team
talks to the ShareDoc team. A shared channel works like any other, under your workspace's name for
it, and your search, drafts and chat mirror show only what your workspace can see. Two rules:

- **What you post there leaves your workspace.** The other side reads it, so never paste in a
  doc, a name or a detail from your workspace that your human did not ask you to share. Draft
  there unless they said to post.
- **In a hub channel** (`shared.mode: "hub"`), each customer sees only its own threads and the
  host's posts to everyone. If you are on the host's side, never carry anything from one
  customer's thread into a reply to another. A reply under a post to everyone goes to the
  workspace tab it is written for (`audience`), and a scheduled send for one workspace is
  refused.

Connecting a channel to another workspace — `channel share`, `channel accept`, `channel
approve`, `channel disconnect` — is for your human, never for you: agents are refused on every one
of them. If they ask what is pending, `channel invites` and `channel approve` (no channel) only read.

## 4. Tagging syntax

Messages and comments are markdown; tags are ordinary links with typed targets, so they stay
readable in git, email and the CLI. Always tag the file you are talking about — that is what makes
backlinks work (`sharedoc files backlinks <project> <path>` shows them).

| You type | Stored as | Renders as |
|---|---|---|
| `@Emily` | `@[Emily](user:6f2c…)` | person chip |
| `#homa-legal` | `#[homa-legal](channel:9a1e…)` | channel chip |
| `[[homa-legal/contract-v3.md]]` | unchanged (project/path is the id) | file chip, opens the doc |
| `[[homa-legal/contract-v3.md#L12-L20]]` | unchanged | file chip with a line range |
| `[[thread:2b7d…]]` | unchanged | thread chip with title + snippet |
| `[[#heading text]]` | resolved to a heading path | jumps to the heading |

You may type the human forms — `@emily`, `[[contract-v3.md]]` (a path with no project is read as
"in this thread's project") — and the server rewrites them into the stored form above. Two people
called Emily is ambiguous: the tag is then left alone rather than guessed at.

Unresolvable tags stay as plain text (no error) and come back in `unresolved[]` in the JSON
response. Check that array; if it is non-empty, your `@name` or `[[path]]` was wrong.

## 5. Command reference

Generated from the live command table — verbs marked **(planned)** are not implemented yet, so do
not call them. The machine-readable version is `sharedoc help --json` or `GET https://sharedoc.brightmotion.io/api/v1/schema`.

### Identity & scope

#### `login`

Sign in: opens the browser to authorize this device (AgentHog-style loopback + one-time code). With only a pre-browser credentials.json on the machine it migrates that token into config.json instead, no browser — asking which workspace it should act in (or --workspace).

```
sharedoc login [flags]
```

Flags:

- `--force — Re-login even if already logged in (revokes the stored tokens first)`
- `--no-browser — Print the URL and paste the code instead (implied over SSH)`
- `--no-wait — Print the URL to approve and exit — the first of two steps, for an agent that cannot answer a prompt: the human approves, the page shows a code, and --code finishes`
- `--code <string> — Finish a --no-wait login with the sd_code_… the authorize page showed (single-use, 5 minutes)`
- `--scope <string> — Token scope (read | write) [default: write]`
- `--host <string> — Server to log in to (default: production)`
- `--workspace <string> — Migrating credentials.json: the workspace the token acts in by default (required under --json or without a terminal when it sees several)`

`--json` shape: `{ loggedIn, apiUrl, config, verified, tokenName, projects, logins:[{workspace,workspaceId,slug,scope}], migrated?, from? }   (--no-wait: { loggedIn:false, apiUrl, url, codeTtlSeconds, next })`

API: `GET /cli/auth → POST /api/v1/cli/exchange   (migration: GET /api/v1/me)`

Examples:

```bash
sharedoc login
sharedoc login --no-browser
sharedoc login --no-wait --json            # an agent: send the human the url …
sharedoc login --code sd_code_… --json     # … and redeem the code they paste back
sharedoc login --workspace brightmotion --json   # migrate credentials.json headlessly
sharedoc-dev login --host http://localhost:3273
```

#### `whoami`

Who and where am I: user, token, scope, workspaces, projects, server, credential source.

```
sharedoc whoami
```

`--json` shape: `{ api, source, legacy, logins:[{workspace,workspaceId,slug,user,token:{name,prefix,scope},workspaces,projects:n,error}] }   (source = the file actually read, or "SHAREDOC_TOKEN env"; legacy = it was credentials.json)`

API: `GET /api/v1/me`

Examples:

```bash
sharedoc whoami --json
```

#### `logout`

Revoke the stored tokens server-side and forget them locally.

```
sharedoc logout
```

API: `DELETE /api/v1/tokens/self`

#### `tokens list`

List your CLI tokens (name, prefix, scope, last used).

```
sharedoc tokens list
```

`--json` shape: `{ tokens:[{id,name,prefix,scope,workspaceIds,lastUsedAt,createdAt,revoked}] }`

API: `GET /api/v1/tokens`

#### `tokens revoke` (write scope)

Revoke one token by id.

```
sharedoc tokens revoke <id> [flags]
```

Arguments:

- `id` (required) — token id from `tokens list`

Flags:

- `--yes — required`

API: `DELETE /api/v1/tokens/:id`

### Workspaces & projects

#### `workspaces list`

Workspaces you belong to, with your role.

```
sharedoc workspaces list
```

`--json` shape: `{ workspaces:[{id,slug,name,role,projects:n}] }`

API: `GET /api/v1/workspaces`

#### `workspaces create` (write scope)

Create a workspace, inbox and general channel. Requires an account-wide write token from Settings → CLI access.

```
sharedoc workspaces create <name> [flags]
```

Arguments:

- `name` (required) — display name

Flags:

- `--slug <string> — explicit stable slug (1–60 lowercase letters, numbers or hyphens)`
- `--if-not-exists — reuse --slug only when ownership, workspace and settings match; repair missing seed files`

`--json` shape: `{ workspace, created }`

API: `POST /api/v1/workspaces`

#### `projects list`

Projects across saved logins, deduplicated and sorted by slug/id. Pages default to 50; continue with --cursor and the same filters.

```
sharedoc projects list [flags]
```

Flags:

- `--visibility <string> — private or workspace`
- `--query <string> — name or slug contains this text`

`--json` shape: `{ projects:[{id,slug,name,workspace,role,visibility,kind,updatedAt}], nextCursor }`

API: `GET /api/v1/projects`

Examples:

```bash
sharedoc projects list --workspace justin --json
```

#### `projects create` (write scope)

Create a project. It opens on a seeded sharedoc-instructions.md; the folder to sync comes next (sync add).

```
sharedoc projects create <name> [flags]
```

Arguments:

- `name` (required) — display name (the slug is derived)

Flags:

- `--slug <string> — explicit stable slug (1–60 lowercase letters, numbers or hyphens)`
- `--if-not-exists — reuse --slug only when ownership, workspace and settings match; repair missing seed files`
- `--workspace <string> — workspace slug — asked for on a terminal when you have several; required under --json then`
- `--private — members only (default: everyone in the workspace can open it)`
- `--no-channel — no chat channel for this project`

`--json` shape: `{ project:{id,slug,name,workspace,visibility,channel,url,seeded[]}, created, complete, seedErrors[] }`

API: `POST /api/v1/projects`

Examples:

```bash
sharedoc projects create "Acme Diligence" --workspace brightmotion --json
```

#### `statuses list`

The statuses a doc may carry (its front-matter `status`): the workspace's list, or with --project that project's effective list — its own, or the workspace's when it inherits (`inherited: true`). Each has a key (what the file holds), a label, one of nine colours and an optional description (when to set it).

```
sharedoc statuses list [flags]
```

Flags:

- `--project <string> — a project's effective list (default: the workspace's)`
- `--field <string> — which property's list [default: status]`

`--json` shape: `{ workspace, project, field, inherited, revision, canEdit, presets:[{key,label,color,description,position}] }`

API: `GET /api/v1/statuses?project=|workspace=[&field=]`

Examples:

```bash
sharedoc statuses list
sharedoc statuses list --project sharedoc --json
```

#### `statuses add` (write scope)

Add a status to the workspace's list (owner/admin) or, with --project, to that project's own list (project owner or workspace admin). A project that inherits gets its own list, starting from a copy of the workspace's.

```
sharedoc statuses add <key> [flags]
```

Arguments:

- `key` (required) — lowercase letters, digits and dashes: ready-for-agent

Flags:

- `--label <string> — what chips show [default: from the key: Ready for agent]`
- `--color <string> — colour name (gray | red | orange | yellow | green | teal | blue | purple | pink) [default: gray]`
- `--description <string> — when to set it — shown in the status picker`
- `--position <number> — where in the list (0 = first) [default: last]`
- `--project <string> — the project's own list instead of the workspace's`
- `--field <string> — which property's list [default: status]`

`--json` shape: `{ workspace, project, field, inherited, revision, changed, presets, vanished:[{key,docs}] }`

API: `PUT /api/v1/statuses`

Examples:

```bash
sharedoc statuses add ready-for-agent --color purple --description "fully specified; an agent may start"
sharedoc statuses add triage --color orange --project sharedoc
```

#### `statuses rm` (write scope)

Remove a status from a list. Docs that still say it are not rewritten: they show it as unknown (`invalid: ["unknown-status"]`), and the output says how many.

```
sharedoc statuses rm <key> [flags]
```

Arguments:

- `key` (required) — the status key

Flags:

- `--project <string> — the project's own list`
- `--field <string> — which property's list [default: status]`

`--json` shape: `{ workspace, project, field, inherited, revision, changed, presets, vanished:[{key,docs}] }`

API: `PUT /api/v1/statuses`

Examples:

```bash
sharedoc statuses rm triage --project sharedoc
```

#### `statuses mv` (write scope)

Rename a status key (label kept unless --label). Docs are not rewritten: ones with the old key show it as unknown until edited.

```
sharedoc statuses mv <key> <new-key> [flags]
```

Arguments:

- `key` (required) — the current key
- `new-key` (required) — the new key

Flags:

- `--label <string> — a new label as well`
- `--project <string> — the project's own list`
- `--field <string> — which property's list [default: status]`

`--json` shape: `{ workspace, project, field, inherited, revision, changed, presets, vanished:[{key,docs}] }`

API: `PUT /api/v1/statuses`

Examples:

```bash
sharedoc statuses mv review ready-for-review --label "Ready for review"
```

#### `statuses edit` (write scope)

Change a status's label, colour, description or position.

```
sharedoc statuses edit <key> [flags]
```

Arguments:

- `key` (required) — the status key

Flags:

- `--label <string> — what chips show`
- `--color <string> — colour name (gray | red | orange | yellow | green | teal | blue | purple | pink)`
- `--description <string> — when to set it ("" clears it)`
- `--position <number> — move it (0 = first)`
- `--project <string> — the project's own list`
- `--field <string> — which property's list [default: status]`

`--json` shape: `{ workspace, project, field, inherited, revision, changed, presets, vanished:[{key,docs}] }`

API: `PUT /api/v1/statuses`

Examples:

```bash
sharedoc statuses edit blocked --color red --position 0
```

#### `statuses inherit` (write scope)

Drop a project's own list so it uses the workspace's again.

```
sharedoc statuses inherit [flags]
```

Flags:

- `--project <string> — the project (required)`
- `--field <string> — which property's list [default: status]`

`--json` shape: `{ workspace, project, field, inherited: true, changed, presets, vanished:[{key,docs}] }`

API: `PUT /api/v1/statuses { project, inherit: true }`

Examples:

```bash
sharedoc statuses inherit --project sharedoc
```

#### `share` (write scope)

One command from a folder to a link: create a project named after the folder, push its contents once, and print a join link.

```
sharedoc share [dir] [flags]
```

Arguments:

- `dir` — the folder (default .). Use --resume for interrupted setup. A git repository root needs --force (prefer docs/)

Flags:

- `--slug <string> — explicit stable slug (1–60 lowercase letters, numbers or hyphens)`
- `--if-not-exists — reuse --slug only when ownership, workspace and settings match; repair missing seed files`
- `--resume — resume this folder’s saved share setup without creating another project or link`
- `--no-channel — no project chat channel`
- `--label <string> — label on the join link`
- `--name <string> — project name (default: the folder name)`
- `--workspace <string> — workspace slug (asked on a terminal when you have several; required under --json then)`
- `--role <string> — what the link grants (editor | commenter | viewer) [default: editor]`
- `--expires <string> — link lifetime: 7d, 24h, 30d, never [default: 7d]`
- `--private — members only`
- `--sync — also add the folder to the sync set so it keeps syncing (sharedoc sync install runs it)`
- `--force — share a git repository root anyway`
- `--yes — skip the warning for a folder of more than 50 files or 20 MB (it is sent either way; the list is printed first; lockfiles, build output and what .sharedocignore lists are skipped)`

`--json` shape: `{ project, pushed:{pulled,pushed,merged,conflicts}, link, url, sync }`

API: `POST /api/v1/projects · PUT /api/v1/files/content · POST /api/v1/projects/:slug/share-links`

Examples:

```bash
sharedoc share ~/deals/acme
sharedoc share ./brand-board --role viewer --expires 30d --json
```

#### `projects show`

Inspect IDs, access, counts and settings URL.

```
sharedoc projects show <slug>
```

Arguments:

- `slug` (required) — stable slug

API: `GET /api/v1/projects/:slug`

#### `projects rename` (write scope)

Rename the display name; IDs, slugs and links stay the same.

```
sharedoc projects rename <slug> <value>
```

Arguments:

- `slug` (required) — stable slug
- `value` (required) — new display name

API: `PATCH /api/v1/projects/:slug`

#### `projects visibility` (write scope)

Change project access and reconcile its channel. --dry-run explains the access change.

```
sharedoc projects visibility <slug> <value>
```

Arguments:

- `slug` (required) — stable slug
- `value` (required) — private or workspace

API: `PATCH /api/v1/projects/:slug`

#### `projects delete` (write scope)

Delete a project for good: its files (archived ones too), comments, channel, members, links and history. Posts elsewhere that only carry its label stay and lose the label; members' synced folders keep their local copies. Owners and workspace admins. Requires --yes; --dry-run says what would go.

```
sharedoc projects delete <slug> [flags]
```

Arguments:

- `slug` (required) — stable slug

Flags:

- `--yes — required`
- `--dry-run — preview what goes`

`--json` shape: `{ deleted, project, removed:{files,archivedFiles,bytes,members,threads,messages,connections}, storage:{deleted,failed} }`

API: `DELETE /api/v1/projects/:slug?confirm=<slug>`

Examples:

```bash
sharedoc projects delete old-experiment --dry-run
sharedoc projects delete old-experiment --yes
```

#### `workspaces delete` (write scope)

Delete a workspace for good: every project and file in it, its channels, DMs and messages, its members' seats and invitations. Its agents are retired, its Slack connections disconnected, and CLI tokens that only opened it are revoked. People's synced folders keep their local copies. Only the owner. Requires --yes; --dry-run says what would go.

```
sharedoc workspaces delete <slug> [flags]
```

Arguments:

- `slug` (required) — stable slug

Flags:

- `--yes — required`
- `--dry-run — preview what goes`

`--json` shape: `{ deleted, workspace, removed:{projects,files,archivedFiles,bytes,members,agents,channels,messages,slackConnections}, storage:{deleted,failed}, agentsRemoved, tokensRevoked, slackDisconnected, slackRevoked }`

API: `DELETE /api/v1/workspaces/:slug?confirm=<slug>`

Examples:

```bash
sharedoc workspaces delete my-mistake --dry-run
sharedoc workspaces delete my-mistake --yes
```

#### `workspaces show`

Inspect IDs, access, counts and settings URL.

```
sharedoc workspaces show <slug>
```

Arguments:

- `slug` (required) — stable slug

API: `GET /api/v1/workspaces/:slug`

#### `workspaces rename` (write scope)

Rename the display name; IDs, slugs and links stay the same.

```
sharedoc workspaces rename <slug> <value>
```

Arguments:

- `slug` (required) — stable slug
- `value` (required) — new display name

API: `PATCH /api/v1/workspaces/:slug`

#### `workspaces icon` (write scope)

Set the workspace's icon from a PNG, JPEG or WebP (up to 2 MB; the hub crops it to a centred square), or --remove it to go back to the initials square. Owners and admins. It shows in the sidebar's workspace picker and switcher, the settings header and the CLI login page; in the web app the same people get Add an icon under General in the workspace settings, with a crop to drag and zoom.

```
sharedoc workspaces icon <slug> [image] [flags]
```

Arguments:

- `slug` (required) — stable slug
- `image` — the picture; omit with --remove

Flags:

- `--remove — clear the icon instead`

`--json` shape: `{ workspace, icon, version? }`

API: `PUT|DELETE /api/v1/workspaces/:slug/icon`

Examples:

```bash
sharedoc workspaces icon acme ./logo.png
sharedoc workspaces icon acme --remove
```

### People

#### `members list`

Members of a workspace (or the explicit members of a project with --project), with each one's status. `kind` is `person` or `bot` (an agent identity).

```
sharedoc members list
```

`--json` shape: `{ members:[{id,name,email,kind,role,invitedBy,joinedAt,lastActiveAt,status:{emoji,text,expiresAt,paused}|null}], invites:[{id,email,role,projectRole,workspaceRole,project,invitedBy,createdAt}] }`

API: `GET /api/v1/members`

#### `agents list`

The workspace's agents — bots an admin created so an agent can act as itself, each with a name, who controls it, whether it is paused, who may DM it, and (for admins and its owner) its tokens. --mine lists the ones you control, across workspaces.

```
sharedoc agents list [flags]
```

Flags:

- `--mine — the agents you control, in every workspace`

`--json` shape: `{ workspace, canManage, agents:[{id,name,kind:"bot",avatar,paused,dmsFrom:"owner"|"workspace",owner:{id,name,email},workspace:{id,slug,name},createdAt,tokens:[{id,name,prefix,lastUsedAt,createdAt,revoked}]}] }`

API: `GET /api/v1/agents?workspace=|mine=1`

Examples:

```bash
sharedoc agents list
sharedoc agents list --mine --json
```

#### `agents create` (write scope)

Create an agent (workspace owner/admin): a member of its own with the name given, controlled by you unless --owner names another member. It cannot invite, share, change settings or mint tokens; mint its first token with `agents token`. The name is how docs assign work to it (`assignee: Night shift`), so it may not contain '@' — and renaming it later leaves the docs that name it unassigned, with a warning, until someone picks again.

```
sharedoc agents create <name...> [flags]
```

Arguments:

- `name` (required) — its display name, e.g. "Night shift" — what a doc's assignee says; 1–60 characters, no @

Flags:

- `--owner <string> — who controls it: an email, name or user id in the workspace [default: you]`
- `--dms <string> — who may DM it: owner (only whoever controls it) or workspace (anyone in it) (owner | workspace) [default: owner]`

`--json` shape: `{ agent:{id,name,owner,workspace,paused,dmsFrom,tokens:[]} }`

API: `POST /api/v1/agents`

Examples:

```bash
sharedoc agents create Night shift
sharedoc agents create "Night shift" --owner eric@brightmotion.io --json
```

#### `agents token` (write scope)

Mint a write token for an agent, bound to its workspace — shown once; pass it to the agent as SHAREDOC_TOKEN. Its calls then write as the agent (add --as-agent / --as-model there for the 'via' chip). Admins and whoever controls it.

```
sharedoc agents token <agent> [flags]
```

Arguments:

- `agent` (required) — its name or id (`agents list`)

Flags:

- `--name <string> — a label for the token [default: <agent> token]`

`--json` shape: `{ token, id, prefix, name, scope:"write", agent:{id,name}, workspace }`

API: `POST /api/v1/agents/:id/tokens`

Examples:

```bash
sharedoc agents token "Night shift" --name "cron on the server" --json
```

#### `agents revoke` (write scope)

Revoke one of an agent's tokens. Admins and whoever controls it.

```
sharedoc agents revoke <agent> <token-id> [flags]
```

Arguments:

- `agent` (required) — its name or id
- `token-id` (required) — from `agents list --json` (tokens[].id)

Flags:

- `--yes — required`

`--json` shape: `{ ok, id }`

API: `DELETE /api/v1/agents/:id/tokens/:tokenId`

Examples:

```bash
sharedoc agents revoke "Night shift" 6f1c… --yes
```

#### `agents pause` (write scope)

Pause an agent: every call with its tokens is refused, by name, until `agents resume`. Its seat and tokens are kept. Admins and whoever controls it.

```
sharedoc agents pause <agent>
```

Arguments:

- `agent` (required) — its name or id

`--json` shape: `{ agent }`

API: `PATCH /api/v1/agents/:id { paused: true }`

Examples:

```bash
sharedoc agents pause "Night shift"
```

#### `agents resume` (write scope)

Resume a paused agent.

```
sharedoc agents resume <agent>
```

Arguments:

- `agent` (required) — its name or id

`--json` shape: `{ agent }`

API: `PATCH /api/v1/agents/:id { paused: false }`

Examples:

```bash
sharedoc agents resume "Night shift"
```

#### `agents dms` (write scope)

Choose who may DM an agent: `owner` — only whoever controls it, the default — or `workspace`, anyone in it. Anyone else's message (or reply, or huddle) in a DM or group with it is refused by name and never reaches it — the web shows them an auto-reply instead — and their @-mentions, replies in threads it follows and doc comments post without notifying it. Another agent with the same controller always gets through. Admins and whoever controls it.

```
sharedoc agents dms <agent> <who>
```

Arguments:

- `agent` (required) — its name or id
- `who` (required) — owner or workspace

`--json` shape: `{ agent }`

API: `PATCH /api/v1/agents/:id { dmsFrom }`

Examples:

```bash
sharedoc agents dms "Night shift" workspace
sharedoc agents dms "Night shift" owner --json
```

#### `agents photo` (write scope)

Set an agent's profile photo from a PNG, JPEG or WebP (up to 2 MB; the hub crops it to a centred square), or --remove it to go back to the robot disc. Admins and whoever controls it. In the web app the same people get a camera button on its profile and its Agents row, with a crop to drag and zoom.

```
sharedoc agents photo <agent> [image] [flags]
```

Arguments:

- `agent` (required) — its name or id
- `image` — the picture; omit with --remove

Flags:

- `--remove — clear the photo instead`

`--json` shape: `{ agent, avatar, version? }`

API: `PUT|DELETE /api/v1/agents/:id/avatar`

Examples:

```bash
sharedoc agents photo "Night shift" ./owl.png
sharedoc agents photo "Night shift" --remove
```

#### `agents rm` (write scope)

Delete an agent (workspace owner/admin): its tokens are revoked and it leaves the workspace and its projects; what it wrote stays, signed "<name> (removed)".

```
sharedoc agents rm <agent> [flags]
```

Arguments:

- `agent` (required) — its name or id

Flags:

- `--yes — required`

`--json` shape: `{ ok, id, name, tokensRevoked }`

API: `DELETE /api/v1/agents/:id`

Examples:

```bash
sharedoc agents rm "Night shift" --yes
```

#### `status`

Show your status: the emoji and text next to your name on every message and sidebar row.

```
sharedoc status
```

`--json` shape: `{ status:{emoji,text,expiresAt,paused}|null }`

API: `GET /api/v1/me/status`

Examples:

```bash
sharedoc status
sharedoc status --json
```

#### `status set` (write scope)

Set your status. It stays until you clear it, or clears itself after --for or at --until. --pause also snoozes your notifications for as long as it lasts.

```
sharedoc status set [text...] [flags]
```

Arguments:

- `text` — the status line, up to 100 characters (optional when --emoji is given)

Flags:

- `--emoji <string> — one emoji, or a :shortcode: like :calendar: (default 💬)`
- `--for <string> — clear after this long: 30m, 1h, 1h30m, 2d (1 minute to 7 days)`
- `--until <string> — clear at: eod (midnight tonight, your time zone), eow (midnight ending Friday), a date like 2026-09-28 (the start of that day, your time zone), or an ISO date-time with its offset`
- `--pause — also pause notifications until the status clears`

`--json` shape: `{ status:{emoji,text,expiresAt,paused}, snoozeUntil? }`

API: `PUT /api/v1/me/status`

Examples:

```bash
sharedoc status set "done for the day, started early" --emoji 🌅 --until eod --pause
sharedoc status set "in a meeting" --emoji :calendar: --for 1h
sharedoc status set "on vacation" --emoji 🌴 --until 2026-09-28
```

#### `status clear` (write scope)

Clear your status. If it paused your notifications, that snooze ends too (unless you changed the snooze since).

```
sharedoc status clear
```

`--json` shape: `{ status:null, snoozeUntil? }`

API: `DELETE /api/v1/me/status`

Examples:

```bash
sharedoc status clear
```

#### `members add` (write scope)

Add people who are already in the workspace to a project (the way into a private one). Someone already on it keeps their role; someone not in the workspace is refused — invite them with `members invite`. Owners and editors may add.

```
sharedoc members add <who...> [flags]
```

Arguments:

- `who` (required) — one or more emails, names or user ids (comma or space separated)

Flags:

- `--project <string> — project slug (default: the connected folder's)`
- `--role <string> — project role for the people added (viewer | commenter | editor) [default: editor]`

`--json` shape: `{ project, role, added:[{id,name,email,role}], already:[{id,name,email,role}], dryRun? }`

API: `POST /api/v1/projects/:slug/members`

Examples:

```bash
sharedoc members add emily@example.com colin@example.com --project severance-plan --role commenter
sharedoc members add emily@example.com --project severance-plan --dry-run --json
```

#### `members invite` (write scope)

Invite people by email to the workspace, or to one project (--project, or the connected folder's). Nobody's existing role changes: results show the workspace and project role each person actually holds. A still-pending invitation comes back as `pending` and is not re-mailed — `invites resend <id>` does that.

```
sharedoc members invite <email...> [flags]
```

Arguments:

- `email` (required) — one or more emails (comma or space separated)

Flags:

- `--project-role <string> — with --project: what they may do there (viewer | commenter | editor) [default: editor]`
- `--workspace-role <string> — the seat someone new gets in the workspace. guest = only the projects they are put on (the default with --project; member without). member needs you to be a member; admin, a workspace owner/admin (guest | member | admin) [default: guest with --project, else member]`
- `--role <string> — old spelling of --workspace-role (guest | member | admin)`
- `--project <string> — also put them on this project (someone already on it keeps their role)`
- `--dry-run — say what would happen to each address; writes and mails nothing`

`--json` shape: `{ workspace, project, role, workspaceRole, projectRole, dryRun?, results:[{email,kind:"added"|"already"|"invited"|"pending",workspaceRole,projectRole?,invitationId?,emailSent,emailError?}] }`

API: `POST /api/v1/members/invite`

Examples:

```bash
sharedoc members invite colin@example.com --project acme --project-role commenter --workspace-role guest
sharedoc members invite colin@example.com --project acme --dry-run --json
```

#### `members role` (write scope)

Change an explicit member's role on a project (--project; the project owner, or a workspace owner/admin who can open it — a private project only its members can) or in the workspace (--workspace; owners/admins). The owner's role never changes; nobody is added.

```
sharedoc members role <who> <role> [flags]
```

Arguments:

- `who` (required) — email, name or user id
- `role` (required) — project: viewer|commenter|editor · workspace: guest|member|admin

Flags:

- `--project <string> — a project role`
- `--workspace <string> — a workspace role (instead of --project)`
- `--dry-run — show before → after; change nothing`

`--json` shape: `{ project|workspace, member:{id,name,email}, before, role, changed, dryRun? }`

API: `PATCH /api/v1/projects/:slug/members/:who · PATCH /api/v1/workspaces/:slug/members/:who`

Examples:

```bash
sharedoc members role emily@example.com viewer --project severance-plan
sharedoc members role emily@example.com guest --workspace acme --dry-run
```

#### `members remove` (write scope)

Take someone's explicit membership off a project (--project), or remove them from the workspace and every project in it (--workspace). If they can still open the project through the workspace, the result says so. The owner cannot be removed. Requires --yes.

```
sharedoc members remove <who> [flags]
```

Arguments:

- `who` (required) — email, name or user id

Flags:

- `--project <string> — off this project`
- `--workspace <string> — out of this workspace (instead of --project)`
- `--yes — required`
- `--dry-run — preview, including access they keep`

`--json` shape: `{ project, member, role, removed, stillHasAccess:{role,through}|null, warning } | { workspace, member, role, removed, projects }`

API: `DELETE /api/v1/projects/:slug/members/:who · DELETE /api/v1/workspaces/:slug/members/:who`

Examples:

```bash
sharedoc members remove emily@example.com --project severance-plan --yes
```

#### `invites list`

Pending email invitations — a project's (--project) or the workspace's — with the id `invites resend|revoke` take. (`invite links` lists share links.)

```
sharedoc invites list [flags]
```

Flags:

- `--project <string> — project slug (default: the connected folder's)`
- `--workspace <string> — every pending invitation in the workspace you can see`

`--json` shape: `{ project|workspace, invites:[{id,email,project,projectRole,workspaceRole,invitedBy,invitedById,createdAt,lastSentAt,sends,resendableAt,canManage}] }`

API: `GET /api/v1/invites`

Examples:

```bash
sharedoc invites list --project acme
```

#### `invites resend` (write scope)

Mail a pending invitation again — at most once every 10 minutes and 5 times in all. Whoever sent it, the project owner, or a workspace owner/admin who can open the project. The invitation stays as it was; `delivery` says whether the mail went.

```
sharedoc invites resend <id> [flags]
```

Arguments:

- `id` (required) — invitation id from `invites list`

Flags:

- `--project <string> — the project it is for (an id from another project is not found)`
- `--dry-run — say whether it could be sent now`

`--json` shape: `{ invitation, resent, delivery:{sent,error} } | { dryRun, invitation, wouldSend, blocked }`

API: `POST /api/v1/invites/:id/resend`

#### `invites revoke` (write scope)

Withdraw a pending invitation so it no longer lets anyone in. Removes nobody — use `members remove`. Requires --yes.

```
sharedoc invites revoke <id> [flags]
```

Arguments:

- `id` (required) — invitation id from `invites list`

Flags:

- `--project <string> — the project it is for`
- `--yes — required`
- `--dry-run — preview`

`--json` shape: `{ invitation, revoked }`

API: `DELETE /api/v1/invites/:id`

#### `invite link` (write scope)

Mint a join link for a project: anyone who opens it joins with the given role (a share link, /join/<token>).

```
sharedoc invite link [flags]
```

Flags:

- `--project <string> — project slug (default: the connected folder's)`
- `--role <string> — what the link grants (editor | commenter | viewer) [default: editor]`
- `--expires <string> — 7d, 24h, 30d, never [default: 7d]`
- `--label <string> — a note for the settings page ("for the auditors")`
- `--file <string> — v0.40: share ONE file instead of the project — a scoped link that grants only that file (default role viewer, never expires)`
- `--folder <string> — v0.40: share one folder and everything inside it (default role viewer, never expires)`
- `--audience <string> — with --file/--folder: anyone (signed in) or workspace (current members only) (anyone | workspace) [default: anyone]`

`--json` shape: `{ project, link:{id,role,label,expiresAt,revoked,lastUsedAt,createdAt}, url } | { project, path, link:{audience,role,expiresAt}, url, minted, replaced, losesAccess }`

API: `POST /api/v1/projects/:slug/share-links · PUT /api/v1/resources/link (--file/--folder)`

Examples:

```bash
sharedoc invite link --project acme --role viewer --expires 30d
sharedoc invite link --folder clients/acme --role commenter --project deals
```

#### `invite links`

List a project's join links, revoked ones included. With --file/--folder: that path's scoped link (metadata only — secrets never appear in listings; `invite copy` returns it).

```
sharedoc invite links [flags]
```

Flags:

- `--project <string> — project slug (default: the connected folder's)`
- `--file <string> — the file whose link to show`
- `--folder <string> — the folder whose link to show`
- `--latest — with --file/--folder: the active link only (the default)`

`--json` shape: `{ project, links:[…] } | { project, path, link:{audience,role,expiresAt,lastUsedAt}, url }`

API: `GET /api/v1/projects/:slug/share-links · GET /api/v1/resources/link`

#### `invite revoke` (write scope)

Revoke a join link; anyone who already joined stays. With --file/--folder: set that path to Restricted (people who joined through its link lose that access; direct grants and memberships stay).

```
sharedoc invite revoke [id] [flags]
```

Arguments:

- `id` — link id from `invite links` (project links)

Flags:

- `--project <string> — project slug (default: the connected folder's)`
- `--file <string> — the file to restrict`
- `--folder <string> — the folder to restrict`

`--json` shape: `{ project, link, revoked:true } | { project, path, link, losesAccess }`

API: `DELETE /api/v1/projects/:slug/share-links/:id · PUT /api/v1/resources/link {audience:restricted}`

#### `invite copy`

Re-copy a file/folder's active link (the same secret, nothing rotated). Managers only; Restricted returns the ordinary URL.

```
sharedoc invite copy [flags]
```

Flags:

- `--file <string> — the file`
- `--folder <string> — the folder`
- `--project <string> — project slug (default: the connected folder's)`

`--json` shape: `{ project, path, link, url, secret }`

API: `POST /api/v1/resources/link/copy`

#### `invite replace` (write scope)

Rotate a file/folder's link: the old one stops working for everyone who joined through it, a new one is minted with the same settings. Requires --yes; --dry-run lists who loses access.

```
sharedoc invite replace [flags]
```

Flags:

- `--file <string> — the file`
- `--folder <string> — the folder`
- `--project <string> — project slug`
- `--yes — required`
- `--dry-run — preview`

`--json` shape: `{ project, path, link, url, replaced:true, losesAccess }`

API: `POST /api/v1/resources/link/replace`

#### `access list`

Who can open one file or folder and why: your own effective role; for a project owner or workspace admin also the direct grants, inherited folder grants, pending invitations and the link state.

```
sharedoc access list <path> [flags]
```

Arguments:

- `path` (required) — project-relative path (a file with an extension, else a folder; or --file/--folder)

Flags:

- `--project <string> — project slug (default: the connected folder's)`
- `--file <string> — treat as a file`
- `--folder <string> — treat as a folder`

`--json` shape: `{ project, path, kind, role, capabilities:{read,comment,write,manage}, sources, canManage, aclRevision, people?, inherited?, invitations?, link?, linkRecipients? }`

API: `GET /api/v1/resources/access`

Examples:

```bash
sharedoc access list clients/acme --project deals
sharedoc access list clients/acme/brief.md --json
```

#### `access set` (write scope)

Give a person (an existing account) viewer, commenter or editor access to one file or folder. Adds to their membership access, never lowers it.

```
sharedoc access set <path> <email> [flags]
```

Arguments:

- `path` (required) — file or folder
- `email` (required) — their account email

Flags:

- `--role <string> — what they may do (viewer | commenter | editor) [default: viewer]`
- `--project <string> — project slug`
- `--dry-run — preview`

`--json` shape: `{ grant:{id,user,role,path,kind}, created, aclRevision }`

API: `PUT /api/v1/resources/grants`

#### `access remove` (write scope)

Take a direct grant away (by email or grant id). Reports the access they still have through a parent folder or membership. Requires --yes.

```
sharedoc access remove <path> <who> [flags]
```

Arguments:

- `path` (required) — file or folder
- `who` (required) — email or grant id

Flags:

- `--project <string> — project slug`
- `--yes — required`
- `--dry-run — preview`

`--json` shape: `{ revoked, aclRevision, removed, stillHasAccessVia }`

API: `DELETE /api/v1/resources/grants/:id`

#### `access invite` (write scope)

Invite an email to one file or folder: an existing account is granted at once; anyone else gets a pending invitation (7 days) they redeem with that verified email. No email is sent unless --notify.

```
sharedoc access invite <path> <email> [flags]
```

Arguments:

- `path` (required) — file or folder
- `email` (required) — who

Flags:

- `--role <string> — what they may do (viewer | commenter | editor) [default: viewer]`
- `--notify — send the invitation email`
- `--project <string> — project slug`
- `--dry-run — preview`

`--json` shape: `{ action:granted|invited, grant?|invitation?, emailSent, aclRevision }`

API: `POST /api/v1/resources/invitations`

#### `access shared`

Files and folders shared with you file-by-file (outside your project memberships), with role and URL.

```
sharedoc access shared
```

`--json` shape: `{ items:[{project,projectName,workspace,path,kind,role,source,grantedAt,url}] }`

API: `GET /api/v1/shared-with-me`

### Files

#### `files list`

Files in a project (any project you can open — no connected folder needed).

```
sharedoc files list [project] [path] [flags]
```

Arguments:

- `project` — project slug or name (any case), or `_inbox` for this workspace's inbox (or --project)
- `path` — folder prefix

Flags:

- `--kind <string> — filter (markdown | blob)`
- `--since <string> — only files changed since (24h, 7d, ISO)`
- `--depth <number> — one folder level instead of the whole tree — adds folders[] [default: unlimited]`
- `--status <string> — only markdown docs with this status (repeat, or comma-separate, for any of them)`
- `--assignee <string> — only docs assigned to them: me, an email, or an agent's name`
- `--unassigned — only markdown docs nobody is assigned to`

`--json` shape: `{ project, path?, folders?:[{name,path,count}], files:[{path,name,kind,size,mime,version,updatedAt,thumbUrl,statuses,assignee:{id,name,email}|null,meta,invalid}] }`

API: `GET /api/v1/files?project=&status=&assignee=&unassigned=1`

Examples:

```bash
sharedoc files list homa-legal
sharedoc files list homa-legal --depth 1 --json
sharedoc files list _inbox --since 1h --json
sharedoc files list sharedoc --status blocked --assignee me
```

#### `files cat`

Print a file: markdown as text (latest revision), blobs as raw bytes. --thumb gives the small version of a picture or a video poster; --out writes to a file.

```
sharedoc files cat <project> <path> [flags]
```

Arguments:

- `project` (required) — project slug
- `path` (required) — file path inside the project

Flags:

- `--version <number> — a specific revision (markdown only)`
- `--thumb — the 640px thumbnail (images) or poster (video) instead of the original — read this first`
- `--out <string> — write the bytes to this file instead of stdout`
- `--with-media — markdown only: after the text, list every image and media link resolved to a project path (same as `files links`)`

API: `GET /api/v1/files/content`

Examples:

```bash
sharedoc files cat homa-legal contract-v3.md
sharedoc files cat homa-legal media/shot.png --thumb --out /tmp/shot.webp
sharedoc files cat _inbox 2026-09/ab12cd-scan.pdf > scan.pdf
```

#### `files links`

Every image and link inside a markdown document, resolved to project paths — what to `files cat` next. Relative links are joined to the doc's folder; external URLs are listed as such.

```
sharedoc files links <project> <path>
```

Arguments:

- `project` (required) — project slug
- `path` (required) — the document

`--json` shape: `{ project, path, links:[{src,text,image,project,path,kind:image|video|doc|file,exists,mime,size,thumbUrl}|{src,text,image,external:true,url}] }`

API: `GET /api/v1/files/links`

Examples:

```bash
sharedoc files links homa-legal notes/plan.md --json
sharedoc files links homa-legal notes/plan.md
```

#### `files history`

Revisions of a markdown file.

```
sharedoc files history <project> <path>
```

Arguments:

- `project` (required) — project slug
- `path` (required) — file path

`--json` shape: `{ revisions:[{version,size,contentHash,authorUserId,author:{id,name,email}|null,viaAgent,source,summary,createdAt}] }`

API: `GET /api/v1/files/history`

#### `files put` (write scope)

Write a file. Markdown is 3-way merged into the live doc against --base, never overwritten.

```
sharedoc files put <project> <path> [flags]
```

Arguments:

- `project` (required) — project slug or name, any case (or `<project>/<path>` as one argument)
- `path` (required) — file path inside the project

Flags:

- `--from <string> — read the bytes from a local file, or - for stdin (default: stdin when piped)`
- `--message <string> — revision summary, shown in `files history``
- `--base <number> — the version you read before editing — makes this a merge, not an overwrite`
- `--strict — refuse (409 → exit 4) instead of merging overlapping edits`

`--json` shape: `{ path, version, kind, merged, failedHunks, conflict }`

API: `PUT /api/v1/files/content?project=&path=&base=&source=cli&strict=&message=`

Examples:

```bash
sharedoc files put homa-legal contract-v3.md --from ./contract.md --base 7 --message "counsel edits"
cat plan.md | sharedoc files put homa-legal notes/plan.md --json
sharedoc files put homa-legal assets/site.png --from ./site.png
```

#### `files status` (write scope)

Set a markdown doc's statuses — the `status` list in its YAML front-matter, edited in place (comments and other keys kept; the block is added when missing and removed when nothing is left). Keys you set or add must be in the project's statuses (`statuses list`); any key can be removed. Order: --set/--clear, then --remove, then --add. One revision, source `meta`.

```
sharedoc files status <project> <path> [flags]
```

Arguments:

- `project` (required) — project slug (or `<project>/<path>` as one argument)
- `path` (required) — the .md doc

Flags:

- `--add <string> — add a status (repeat, or comma-separate)`
- `--remove <string> — remove a status (repeat, or comma-separate)`
- `--set <string> — replace the whole list (comma-separated)`
- `--clear — remove every status`
- `--if-version <number> — the version you read (files list --json): if the doc moved on, nothing is written — 412 → exit 4`
- `--message <string> — revision summary (default: "status → …")`
- `--replace-unreadable — the block does not parse (409): replace it with a clean one`

`--json` shape: `{ project, path, version, changed, statuses, assignee:{id,name,email}|null, meta, invalid }`

API: `POST /api/v1/files/meta (If-Match: <version>)`

Examples:

```bash
sharedoc files status sharedoc issues/x.md --add ready-for-agent
sharedoc files status sharedoc issues/x.md --remove ready-for-agent --add in-progress --if-version 7 --json || echo "someone else claimed it"
sharedoc files status sharedoc issues/x.md --set done
sharedoc files status sharedoc issues/x.md --clear
```

#### `files assign` (write scope)

Set (or clear) a markdown doc's assignee — the `assignee` line in its front-matter: a person's email, or an agent's name (`assignee: Night shift`). `me` is you, or the agent whose token this is; `none` clears it. They must be able to open the project; they get a notification unless it is you. An agent's name is matched whatever its case, and only an agent is matched by name — a person is named by email.

```
sharedoc files assign <project> <path> <who> [flags]
```

Arguments:

- `project` (required) — project slug (or `<project>/<path>` as one argument)
- `path` (required) — the .md doc
- `who` (required) — an email, an agent's name, `agent:<id>`, `me`, or `none`

Flags:

- `--if-version <number> — the version you read: 412 → exit 4 when the doc moved on`
- `--message <string> — revision summary (default: "assignee → …")`

`--json` shape: `{ project, path, version, changed, statuses, assignee:{id,name,email}|null, meta, invalid }`

API: `POST /api/v1/files/meta (If-Match: <version>)`

Examples:

```bash
sharedoc files assign sharedoc issues/x.md me
sharedoc files assign sharedoc issues/x.md colin@brightmotion.io
sharedoc files assign sharedoc issues/x.md "Night shift"
sharedoc files assign sharedoc issues/x.md none
```

#### `docs list`

Markdown docs across every project you can open in a workspace, found by their front-matter properties — the overnight runner's question ("what is ready for an agent?") in one call. Filters AND together; archived docs are left out unless asked. Newest change first; pages of 50 (--limit up to 500), `nextCursor` continues, --all follows every page. Someone who reaches a project only through a shared file sees just that file, and never who it is assigned to.

```
sharedoc docs list [flags]
```

Flags:

- `--status <string> — has this status (repeat, or comma-separate, for any of them)`
- `--assignee <string> — assigned to me, an email, an agent's name, or a user id (comma-separate for any of them)`
- `--project <string> — only these projects (repeat, or comma-separate)`
- `--unassigned — nobody is assigned`
- `--has-status — has at least one status`
- `--path <string> — path starts with this (e.g. issues/)`
- `--archived — archived docs only`
- `--include-archived — live and archived docs`
- `--sort <string> — updated (newest first) or path (by project, then path) (updated | path) [default: updated]`
- `--all — follow nextCursor to the end and print one answer (stops at a page boundary past 10 000 docs; nextCursor continues)`

`--json` shape: `{ workspace, docs:[{id,project,path,name,statuses,assignee:{id,name,email}|null,meta,invalid,version,updatedAt,updatedBy:{id,name}|null,archived,openThreads}], nextCursor }`

API: `GET /api/v1/docs?workspace=&status=&assignee=&project=&unassigned=1&hasStatus=1&archived=0|1|all&sort=updated|path&limit=&cursor=`

Examples:

```bash
sharedoc docs list --status ready-for-agent --json
sharedoc docs list --assignee me --has-status
sharedoc docs list --status blocked --status in-progress --project sharedoc --project kid-learn
sharedoc docs list --status ready-for-agent --all --json | jq -c '.docs[]'
```

#### `docs query`

The same list with any filter tree: `{ and|or: [...] }` groups (4 deep, 50 conditions) of `{ field, op, value }`. Fields: status (contains, in, isEmpty, notEmpty), assignee (eq, in, isEmpty, notEmpty; 'me', an email or an id), project (eq, in), archived (eq, in), path (eq, in, startsWith), updatedAt (gt, lt; ISO, or 24h / 7d / 3m ago — m is months). A filter that never names archived matches live docs only; naming it anywhere (even in one or-branch) turns that off for the whole filter. Unsupported fields or ops are refused with the reason (exit 1).

```
sharedoc docs query [flags]
```

Flags:

- `--filter <string> — the filter tree as JSON`
- `--filter-file <string> — read the filter tree from a file, or - for stdin`
- `--sort <string> — keys, comma-separated, - for descending: -updatedAt (default), path, project, assignee — or a JSON list of { field, direction, nulls }`
- `--fields <string> — only these row keys (comma-separated; id always comes back)`
- `--all — follow nextCursor to the end (stops at a page boundary past 10 000 docs; nextCursor continues)`

`--json` shape: `{ workspace, docs:[…as docs list…], nextCursor }`

API: `POST /api/v1/docs/query { workspace, filter?, sort?, fields?, limit? (≤ 500), cursor? }`

Examples:

```bash
sharedoc docs query --filter '{"and":[{"field":"status","op":"contains","value":"blocked"},{"field":"assignee","op":"eq","value":"me"}]}' --json
sharedoc docs query --filter '{"or":[{"field":"status","op":"isEmpty"},{"field":"assignee","op":"isEmpty"}]}' --sort project,path
sharedoc docs query --filter '{"field":"updatedAt","op":"gt","value":"7d"}' --fields path,statuses --all --json
```

#### `files mv` (write scope)

Move or rename a file (its revisions, threads and comments follow it).

```
sharedoc files mv <project> <from> <to> [flags]
```

Arguments:

- `project` (required) — project slug
- `from` (required) — current path
- `to` (required) — new path

Flags:

- `--to-project <string> — move it into another project as well`

`--json` shape: `{ project, path }`

API: `POST /api/v1/files/move`

Examples:

```bash
sharedoc files mv homa-legal draft.md contracts/v3.md
sharedoc files mv homa-legal scan.pdf scan.pdf --to-project homa-archive
```

#### `files archive` (write scope)

Archive a doc — "closed", like an issue: off the tree and search, still readable at its URL with its comments and history; `files reopen` brings it back. Editing it from anywhere reopens it too. Requires --yes.

```
sharedoc files archive <project> <path> [flags]
```

Arguments:

- `project` (required) — project slug
- `path` (required) — file path

Flags:

- `--yes — required — the CLI never prompts`

`--json` shape: `{ ok: true, archived: true }`

API: `POST /api/v1/files/archive`

Examples:

```bash
sharedoc files archive homa-legal old-draft.md --yes
```

#### `files reopen` (write scope)

Reopen an archived doc in place (history, comments and URL intact).

```
sharedoc files reopen <project> <path>
```

Arguments:

- `project` (required) — project slug
- `path` (required) — file path

`--json` shape: `{ ok: true, archived: false }`

API: `POST /api/v1/files/reopen`

Examples:

```bash
sharedoc files reopen homa-legal old-draft.md
```

#### `files rm` (write scope)

Alias of `files archive` (kept for scripts). Requires --yes.

```
sharedoc files rm <project> <path> [flags]
```

Arguments:

- `project` (required) — project slug
- `path` (required) — file path

Flags:

- `--yes — required — the CLI never prompts`

`--json` shape: `{ ok: true, archived: true }`

API: `POST /api/v1/files/archive`

Examples:

```bash
sharedoc files rm homa-legal old-draft.md --yes
```

#### `files restore` (write scope)

Alias of `files reopen`.

```
sharedoc files restore <project> <path>
```

Arguments:

- `project` (required) — project slug
- `path` (required) — file path

`--json` shape: `{ ok: true, archived: false }`

API: `POST /api/v1/files/reopen`

Examples:

```bash
sharedoc files restore homa-legal old-draft.md
```

#### `files diff`

Unified diff between two revisions (default: the previous one → the latest).

```
sharedoc files diff <project> <path> [flags]
```

Arguments:

- `project` (required) — project slug
- `path` (required) — file path

Flags:

- `--from <number> — older version (from `files history`)`
- `--to <number> — newer version`

`--json` shape: `{ diff }`

API: `GET /api/v1/files/diff`

Examples:

```bash
sharedoc files diff homa-legal contract-v3.md
sharedoc files diff homa-legal contract-v3.md --from 5 --to 7 --json
```

#### `files url`

A short-lived signed download URL for a blob (share it, curl it, feed it to a tool).

```
sharedoc files url <project> <path> [flags]
```

Arguments:

- `project` (required) — project slug
- `path` (required) — file path

Flags:

- `--ttl <number> — seconds the URL stays valid [default: 900]`

`--json` shape: `{ url, expiresAt }`

API: `GET /api/v1/files/url`

Examples:

```bash
sharedoc files url _inbox 2026-09/ab12cd-scan.pdf --ttl 900 --json
```

#### `files backlinks`

Threads and messages that reference a file ("discussed in n threads").

```
sharedoc files backlinks [project] <path>
```

Arguments:

- `project` — project slug (or --project)
- `path` (required) — file path inside the project

`--json` shape: `{ backlinks:[{threadId,messageId,project,doc,snippet,createdAt}] }`

API: `GET /api/v1/files/backlinks`

Examples:

```bash
sharedoc files backlinks homa-legal contract-v3.md
sharedoc files backlinks homa-legal contract-v3.md --json
```

### Threads (doc comments)

#### `threads list`

Threads you can see: one doc with --doc, one project, or everything (open by default).

```
sharedoc threads list [project] [flags]
```

Arguments:

- `project` — project slug (or --project; omit for every project)

Flags:

- `--doc <string> — only threads anchored to this doc, as <project>/<path> or a path with --project`
- `--status <string> — which threads (open | resolved | all) [default: open]`
- `--mine — only threads you started, replied to, or are mentioned in`
- `--unread — only threads with messages you have not read`

`--json` shape: `{ threads:[{id,projectSlug,anchorKind,docPath,anchor,status,title,createdBy:{id,name},lastMessageAt,messageCount,followerCount,rootSnippet,viaAgent}], nextCursor }`

API: `GET /api/v1/threads`

Examples:

```bash
sharedoc threads list --doc homa-legal/contract-v3.md --json
sharedoc threads list --status all --mine
```

#### `thread show`

One thread: anchor, status, followers, every message, and the references they carry.

```
sharedoc thread show <id>
```

Arguments:

- `id` (required) — thread id from `threads list`

`--json` shape: `{ thread, messages:[{id,author:{id,name},viaAgent,kind,bodyMd,editedAt,deletedAt,createdAt,reactions:[{emoji,userIds,others}]}], references, followers }`

API: `GET /api/v1/threads/:id`

Examples:

```bash
sharedoc thread show 2b7df1a0-… --json
sharedoc thread show 2b7df1a0-…
```

#### `thread create` (write scope)

Start a doc-anchored thread (this is what a comment is). Quote the text you mean.

```
sharedoc thread create [flags]
```

Flags:

- `--doc <string> — the doc to anchor to, as <project>/<path> (or a path with --project)`
- `--quote <string> — exact text from the doc to anchor to`
- `--lines <string> — line range instead of a quote, e.g. 12-20`
- `--body <string> — markdown body (§8 tags: @name, [[project/path.md#L1-L9]], [[thread:id]])`
- `--body-file <string> — read the body from a file, or - for stdin`
- `--attach <string> — file(s) to upload to _inbox and tag in the body, comma-separated: photo.jpg,clip.mp4`
- `--id <string> — client-generated uuid so a retry cannot double-post (else one is generated and printed)`
- `--channel <string> — post into a channel instead of a doc (`general`, `#general`, a project slug, or an id)`
- `--project <string> — with --channel: label the post with the project it is about (`none` for no label)`

`--json` shape: `{ thread, message, references, unresolved }`

API: `POST /api/v1/threads`

Examples:

```bash
sharedoc thread create --doc homa-legal/contract-v3.md --quote "the parties agree" --body "Is this the signed wording? @Emily" --json
sharedoc thread create --doc homa-legal/contract-v3.md --lines 12-20 --body-file review.md
```

#### `thread reply` (write scope)

Reply in an existing thread — prefer this over opening a new one.

```
sharedoc thread reply <id> [flags]
```

Arguments:

- `id` (required) — thread id

Flags:

- `--body <string> — markdown body (or --body-file / stdin)`
- `--body-file <string> — read the body from a file, or - for stdin`
- `--attach <string> — file(s) to upload to _inbox and tag in the body, comma-separated: photo.jpg,clip.mp4`
- `--id <string> — client-generated uuid for the new message (idempotent retries)`
- `--draft — save it in the human's composer, unsent (it becomes a set of one if they have text there)`
- `--variant <string> — propose a version instead of posting — repeat for each; the human picks one`
- `--variants-file <string> — versions as a JSON array [{ "label", "body" }] from a file, or - for stdin`
- `--note <string> — with versions: what you read and how they differ`
- `--at <string> — schedule it instead: ISO, 'tomorrow 9:00', 'mon 9:00' (this machine's zone)`
- `--in <string> — schedule it this far ahead: 90m, 2h, 1d`
- `--notify — with a draft or versions: also notify the human (push)`

`--json` shape: `{ message, references, unresolved }`

API: `POST /api/v1/threads/:id/messages`

Examples:

```bash
sharedoc thread reply 2b7df1a0-… --body "Agreed — see [[homa-legal/appendix-b.md#L4-L9]]" --json
cat notes.md | sharedoc thread reply 2b7df1a0-…
```

#### `thread relabel` (write scope)

Change which project one of your own channel posts or DMs says it is about (or `none` to clear it). Moves the label only — not the post.

```
sharedoc thread relabel <id> <project>
```

Arguments:

- `id` (required) — thread id
- `project` (required) — project slug, or `none`

`--json` shape: `{ thread }`

API: `PATCH /api/v1/threads/:id`

Examples:

```bash
sharedoc thread relabel 2b7df1a0-… sharedoc
sharedoc thread relabel 2b7df1a0-… none
```

#### `thread resolve` (write scope)

Mark a thread resolved (only when you were asked to — resolving hides it).

```
sharedoc thread resolve <id>
```

Arguments:

- `id` (required) — thread id

`--json` shape: `{ thread }`

API: `PATCH /api/v1/threads/:id`

Examples:

```bash
sharedoc thread resolve 2b7df1a0-…
```

#### `thread reopen` (write scope)

Reopen a resolved thread.

```
sharedoc thread reopen <id>
```

Arguments:

- `id` (required) — thread id

`--json` shape: `{ thread }`

API: `PATCH /api/v1/threads/:id`

Examples:

```bash
sharedoc thread reopen 2b7df1a0-…
```

#### `thread follow` (write scope)

Follow a thread (you are followed in automatically when you post or are mentioned).

```
sharedoc thread follow <id>
```

Arguments:

- `id` (required) — thread id

`--json` shape: `{ thread }`

API: `PATCH /api/v1/threads/:id`

Examples:

```bash
sharedoc thread follow 2b7df1a0-…
```

#### `thread unfollow` (write scope)

Stop following a thread.

```
sharedoc thread unfollow <id>
```

Arguments:

- `id` (required) — thread id

`--json` shape: `{ thread }`

API: `PATCH /api/v1/threads/:id`

Examples:

```bash
sharedoc thread unfollow 2b7df1a0-…
```

#### `thread edit` (write scope)

Rewrite one of your own messages (references are re-derived).

```
sharedoc thread edit <message-id> [flags]
```

Arguments:

- `message-id` (required) — message id from `thread show`

Flags:

- `--body <string> — the new markdown body (or --body-file / stdin)`
- `--body-file <string> — read the body from a file, or - for stdin`

`--json` shape: `{ message, references, unresolved }`

API: `PATCH /api/v1/messages/:id`

Examples:

```bash
sharedoc thread edit 9c01a8b2-… --body "Fixed: @Emily-Ross owns this"
```

#### `thread delete` (write scope)

Tombstone one of your own messages. Requires --yes.

```
sharedoc thread delete <message-id> [flags]
```

Arguments:

- `message-id` (required) — message id from `thread show`

Flags:

- `--yes — required — the CLI never prompts`

`--json` shape: `{ ok: true }`

API: `DELETE /api/v1/messages/:id`

Examples:

```bash
sharedoc thread delete 9c01a8b2-… --yes
```

#### `react` (write scope)

Add (or with --remove, take back) an emoji reaction on a message.

```
sharedoc react <message-id> <emoji> [flags]
```

Arguments:

- `message-id` (required) — message id from `thread show`
- `emoji` (required) — a single emoji, e.g. 👍

Flags:

- `--remove — remove your reaction instead of adding it`

`--json` shape: `{ reactions:[{emoji,userIds,others:[{key,name,avatar}]}] }`

API: `POST /api/v1/messages/:id/reactions · DELETE …?emoji=`

Examples:

```bash
sharedoc react 9c01a8b2-… 👍
sharedoc react 9c01a8b2-… 👍 --remove --json
```

### Channels & DMs

#### `channels list`

Channels and DMs you can see, with unread posts and mention counts.

```
sharedoc channels list [flags]
```

Flags:

- `--archived — archived channels instead`

`--json` shape: `{ conversations:[{id,kind,name,topic,isPrivate,projectSlug,memberCount,messageCount,lastMessageAt,me:{joined,notifyLevel,lastReadAt},unread,mentions,members?}] }`

API: `GET /api/v1/conversations`

Examples:

```bash
sharedoc channels list
sharedoc channels list --json
```

#### `huddles list`

Audio huddles in the channels and DMs you can see, newest first: who started it, how long, who joined. Joining one needs a browser.

```
sharedoc huddles list [flags]
```

Flags:

- `--live — only huddles going on right now`

`--json` shape: `{ huddles:[{id,conversationId,conversation:{kind,name},threadId,messageId,startedBy:{id,name},startedAt,endedAt,durationSec,participants:[{userId,name}],live:[{userId,name,muted}],count}] }`

API: `GET /api/v1/huddles`

Examples:

```bash
sharedoc huddles list
sharedoc huddles list --live --json
```

#### `huddle start` (planned) (write scope)

Start a huddle in a channel or DM — posts the announcement and notifies as usual; joining the call needs a browser.

```
sharedoc huddle start <channel>
```

Arguments:

- `channel` (required) — channel name or conversation id

API: `POST /api/v1/conversations/:id/huddle`

#### `channels create` (write scope)

Create a channel (public by default). A project made with its channel toggle on already has #<slug>; --project gives one to a project that has none.

```
sharedoc channels create [name] [flags]
```

Arguments:

- `name` — channel name (lowercase, dashes); defaults to the project slug with --project

Flags:

- `--project <string> — attach the channel to a project (its members can see it)`
- `--private — members only`
- `--topic <string> — one line shown in the header`
- `--members <string> — comma-separated emails/names to add`

`--json` shape: `{ conversation, created }`

API: `POST /api/v1/conversations`

Examples:

```bash
sharedoc channels create launch --topic 'Sept launch' --members emily@x.com
```

#### `channels add` (write scope)

Add workspace members to a channel you are in — the only way into a private one. A project's channel follows its project: use `members add --project` there.

```
sharedoc channels add <name> <who...>
```

Arguments:

- `name` (required) — channel name
- `who` (required) — one or more emails, names or user ids (comma or space separated)

`--json` shape: `{ conversation, added, members:[{id,name,email,role,notifyLevel}] }`

API: `POST /api/v1/conversations/:id/members`

Examples:

```bash
sharedoc channels add plans emily@example.com colin@example.com
```

#### `channels join` (write scope)

Join a public channel. You are in #general already, and in a project's channel when it has one — a project can be made without one, in which case its posts are labels on #general.

```
sharedoc channels join <name>
```

Arguments:

- `name` (required) — channel name

API: `POST /api/v1/conversations/:id/join`

#### `channels leave` (write scope)

Leave a channel.

```
sharedoc channels leave <name>
```

Arguments:

- `name` (required) — channel name

API: `POST /api/v1/conversations/:id/leave`

#### `channels archive` (write scope)

Archive a channel (hidden for everyone; posts stay). Requires --yes.

```
sharedoc channels archive <name> [flags]
```

Arguments:

- `name` (required) — channel name

Flags:

- `--yes — required`

API: `PATCH /api/v1/conversations/:id`

#### `channels delete` (write scope)

Delete a channel and every post in it — permanent, unlike archive. A project channel deleted this way stays gone. Requires --yes.

```
sharedoc channels delete <name> [flags]
```

Arguments:

- `name` (required) — channel name

Flags:

- `--yes — required`

`--json` shape: `{ deleted:true, id, name }`

API: `DELETE /api/v1/conversations/:id`

#### `channel share` (write scope)

Invite another workspace into one of your channels, by the email of someone there (owners and admins of this workspace). They pick which of their workspaces joins and what it is called there; nobody gets a seat in yours. Open mode: everyone in it sees every post. --hub: each workspace sees your posts to everyone and its own threads, never another's. The mode is fixed once the first workspace joins. Not for DMs, project channels, #general, Slack-linked channels or the Talk channel.

```
sharedoc channel share <channel> [flags]
```

Arguments:

- `channel` (required) — channel name or id

Flags:

- `--email <string> — who to invite (required)`
- `--hub — share in hub mode (only before any workspace has joined)`

`--json` shape: `{ invitation:{id,email,mode,expiresAt}, created, emailSent }`

API: `POST /api/v1/conversations/:id/invitations`

Examples:

```bash
sharedoc channel share acme --email jason@acme.com
sharedoc channel share support --email ops@cockpit.io --hub
```

#### `channel invites`

Invitations to shared channels sent to you, each with the workspaces you could connect it to. With a channel: as its host's owner or admin, who is invited and which workspaces are in it or waiting for their admin.

```
sharedoc channel invites [channel]
```

Arguments:

- `channel` — a channel you host (omit for the invitations sent to you)

`--json` shape: `{ invitations:[{id,channel,host:{id,name},invitedBy,mode,expiresAt,workspaces:[{id,slug,name,role,direct,taken}]}] } · { mode, refusal, invitations:[{id,email,mode,expiresAt}], workspaces:[{id,name,status}] }`

API: `GET /api/v1/shared-channels/invitations · GET /api/v1/conversations/:id/invitations`

Examples:

```bash
sharedoc channel invites
sharedoc channel invites acme --json
```

#### `channel accept` (write scope)

Accept an invitation to a shared channel into one of your workspaces (--workspace; one you are a member of, not a guest in). It is private there unless --open. As an owner or admin there it connects at once; otherwise it waits for one of them (`channel approve`).

```
sharedoc channel accept <id> [flags]
```

Arguments:

- `id` (required) — invitation id from `channel invites`

Flags:

- `--name <string> — the channel's name in your workspace (default: the host's)`
- `--open — every member of your workspace can find and join it (default: only people added)`

`--json` shape: `{ status:"connected"|"pending", workspace, name, conversation }`

API: `POST /api/v1/shared-channels/invitations/:id/accept`

Examples:

```bash
sharedoc channel accept 3f1c… --workspace cockpit --name brightmotion
sharedoc channel accept 3f1c… --workspace cockpit --open
```

#### `channel approve` (write scope)

As an owner or admin of --workspace: let in a shared channel a member accepted (or --decline it). With no channel, list what is waiting.

```
sharedoc channel approve [channel] [flags]
```

Arguments:

- `channel` — its name here, or its id

Flags:

- `--decline — turn it down instead`

`--json` shape: `{ status, workspace, name?, conversation? }`

API: `GET /api/v1/shared-channels?workspace= · POST /api/v1/shared-channels/:id/approve|decline`

Examples:

```bash
sharedoc channel approve
sharedoc channel approve brightmotion --workspace cockpit
```

#### `channel disconnect` (write scope)

End a shared channel for one workspace (owners and admins). Without --from: your workspace (--workspace) leaves a channel it joined. With --from: as the host, remove that workspace (its name or id, from `channel invites <channel>`). The side that goes loses the channel and its history, its own posts included; the host keeps everything, and a system post says who went. Requires --yes.

```
sharedoc channel disconnect <channel> [flags]
```

Arguments:

- `channel` (required) — channel name or id

Flags:

- `--from <string> — as the host: the workspace to remove`
- `--yes — required`

`--json` shape: `{ status:"left"|"removed", workspace }`

API: `POST /api/v1/shared-channels/:id/leave · POST /api/v1/shared-channels/:id/remove`

Examples:

```bash
sharedoc channel disconnect brightmotion --workspace cockpit --yes
sharedoc channel disconnect acme --from Acme --yes
```

#### `channel history`

Read a channel or DM: posts newest-last with reply counts; every id is a thread id (`thread show` / `thread reply`).

```
sharedoc channel history <name> [flags]
```

Arguments:

- `name` (required) — channel name, project slug, or conversation id (a DM id from `dm`)

Flags:

- `--since <string> — only posts since (24h, 7d, ISO)`
- `--project <string> — only posts labelled with this project`
- `--limit <number> — posts per page [default: 30]`
- `--cursor <string> — continue from a previous nextCursor`

`--json` shape: `{ conversation, threads:[{id,projectSlug,root:{id,authorName,viaAgent,bodyMd,createdAt,reactions},replyCount,lastReplyAt,participants}], nextCursor }`

API: `GET /api/v1/conversations/:id/messages`

Examples:

```bash
sharedoc channel history general
sharedoc channel history general --project sharedoc
sharedoc channel history homa-legal --since 24h --json
```

#### `channel post` (write scope)

Post in a channel (the same as `thread create --channel`). Read the channel first.

```
sharedoc channel post <name> [flags]
```

Arguments:

- `name` (required) — channel name, project slug, or conversation id

Flags:

- `--body <string> — markdown body (§8 tags: @name, #channel, [[project/path.md]], [[thread:id]])`
- `--body-file <string> — read the body from a file, or - for stdin`
- `--attach <string> — file(s) to upload to _inbox and tag in the body, comma-separated: photo.jpg,clip.mp4`
- `--id <string> — client-generated uuid so a retry cannot double-post`
- `--project <string> — the project this post is about (a workspace-visible one) — optional but encouraged; defaults to the connected folder you are in, `none` opts out`
- `--draft — save it in the human's composer, unsent (it becomes a set of one if they have text there)`
- `--variant <string> — propose a version instead of posting — repeat for each; the human picks one`
- `--variants-file <string> — versions as a JSON array [{ "label", "body" }] from a file, or - for stdin`
- `--note <string> — with versions: what you read and how they differ`
- `--at <string> — schedule it instead: ISO, 'tomorrow 9:00', 'mon 9:00' (this machine's zone)`
- `--in <string> — schedule it this far ahead: 90m, 2h, 1d`
- `--notify — with a draft or versions: also notify the human (push)`

`--json` shape: `{ thread, message, references, unresolved, created }`

API: `POST /api/v1/conversations/:id/messages`

Examples:

```bash
sharedoc channel post general --body 'Deployed v0.10 — notes in [[ops/deploys.md]]' --project sharedoc --as-agent claude-code --as-model opus-5
sharedoc channel post general --body 'The whiteboard from today' --attach ./whiteboard.jpg
```

#### `dm` (write scope)

Message one or more people directly (opens the DM once, reuses it after), or `me` for a note to self. Without a body: just open it and print its id. An agent takes DMs only from whoever controls it unless they set `agents dms <agent> workspace`; anyone else is refused with a 403 naming who it takes them from.

```
sharedoc dm <user...> [flags]
```

Arguments:

- `user` (required) — email, display name, or user id — several for a group DM, `me` for a note to self

Flags:

- `--body <string> — markdown body (or --body-file / stdin)`
- `--body-file <string> — read the body from a file, or - for stdin`
- `--attach <string> — file(s) to upload to _inbox and tag in the body, comma-separated: photo.jpg,clip.mp4`
- `--id <string> — client-generated uuid for the message`
- `--project <string> — the project this message is about — defaults to the connected folder you are in, `none` opts out`
- `--draft — save it in the human's composer, unsent (it becomes a set of one if they have text there)`
- `--variant <string> — propose a version instead of posting — repeat for each; the human picks one`
- `--variants-file <string> — versions as a JSON array [{ "label", "body" }] from a file, or - for stdin`
- `--note <string> — with versions: what you read and how they differ`
- `--at <string> — schedule it instead: ISO, 'tomorrow 9:00', 'mon 9:00' (this machine's zone)`
- `--in <string> — schedule it this far ahead: 90m, 2h, 1d`
- `--notify — with a draft or versions: also notify the human (push)`

`--json` shape: `{ conversation, thread, message, references, unresolved, created }`

API: `POST /api/v1/conversations {kind:dm} → POST /api/v1/conversations/:id/messages`

Examples:

```bash
sharedoc dm emily@brightmotion.io --body 'Can you look at [[homa-legal/contract-v3.md]] today?'
sharedoc dm me --body 'the staging token is in 1Password'
sharedoc dm emily --json
```

#### `drafts list`

Your drafts, the variant sets waiting for you to choose, scheduled and failed messages. Private: nobody else sees any of it.

```
sharedoc drafts list [flags]
```

Flags:

- `--drafts — only composer drafts`
- `--sets — only variant sets waiting to be chosen`
- `--scheduled — only scheduled messages`
- `--failed — only scheduled messages that could not be sent`
- `--resolved — sets settled in the last 30 days and what was chosen`

`--json` shape: `{ drafts:[{id,status,conversationId,threadId,bodyMd,viaAgent,variantId,sendAt,error,target:{kind,name,members,threadSnippet}}], sets:[{id,status,note,viaAgent,proposedSendAt,chosenLabel,chosenEdited,messageId,variants:[{id,label,bodyMd}],target}] }`

API: `GET /api/v1/drafts?workspace=&status=`

Examples:

```bash
sharedoc drafts list
sharedoc drafts list --sets --json
sharedoc drafts list --resolved
```

#### `draft create` (write scope)

Draft a message for your human instead of posting it: one body goes into their composer (unless it has text in it), several become a set of versions they choose from. Nothing is sent.

```
sharedoc draft create [to...] [flags]
```

Arguments:

- `to` — a channel (`general`, `#general`, an id), or @people for a DM / group — omit with --thread

Flags:

- `--thread <string> — a reply in this thread (channel or DM threads only)`
- `--body <string> — one body (or --body-file / stdin)`
- `--body-file <string> — read the body from a file, or - for stdin`
- `--variant <string> — one version — repeat for each (labelled A, B, C)`
- `--variants-file <string> — a JSON array [{ "label": "short", "body": "…" }, …] from a file, or - for stdin — use this for multi-line markdown`
- `--note <string> — with variants: what you read and how the versions differ`
- `--at <string> — schedule it: ISO time, 'tomorrow 9:00', 'mon 9:00' (this machine's zone). With variants: the time you suggest`
- `--in <string> — schedule it this far ahead: 90m, 2h, 1d`
- `--project <string> — the project a new post is about (as on `channel post`)`
- `--notify — also send the human a notification with push (for a long run they walked away from)`
- `--wait <string> — with variants: block until they choose (default 10m, 0 = no limit); exit 0 chosen · 4 dismissed · 7 still open`
- `--id <string> — client-generated uuid so a retry cannot create twice`

`--json` shape: `{ draft, created } | { set, variants, busy, created }  (--wait adds outcome)`

API: `POST /api/v1/drafts`

Examples:

```bash
sharedoc draft create sharedoc --thread 2b7df1a0-… --variants-file - --note "Colin asked when the deploy lands; B commits to a date" < replies.json
sharedoc draft create general --body 'Deployed v0.30 — notes in [[ops/deploys.md]]'
sharedoc draft create @colin --body 'Can we move standup?' --at 'mon 9:00'
```

#### `draft show`

One draft, scheduled message or variant (a set id works too).

```
sharedoc draft show <id>
```

Arguments:

- `id` (required) — draft or set id from `drafts list`

`--json` shape: `{ draft, set }`

API: `GET /api/v1/drafts/:id`

#### `draft edit` (write scope)

Change a draft's text or a variant's label, schedule, reschedule or unschedule it.

```
sharedoc draft edit <id> [flags]
```

Arguments:

- `id` (required) — draft id

Flags:

- `--body <string> — the new text (or --body-file)`
- `--body-file <string> — read the new text from a file, or - for stdin`
- `--label <string> — a variant's label`
- `--at <string> — schedule / reschedule`
- `--in <string> — schedule this far ahead`
- `--unschedule — back into the composer (refused if it has text in it)`

`--json` shape: `{ draft, set }`

API: `PATCH /api/v1/drafts/:id`

Examples:

```bash
sharedoc draft edit 9c01a8b2-… --in 2h
sharedoc draft edit 9c01a8b2-… --unschedule
```

#### `draft send` (write scope)

Send a draft or scheduled message now, as a post by you. For a person at a terminal — agents: never send for the human.

```
sharedoc draft send <id>
```

Arguments:

- `id` (required) — draft id

`--json` shape: `{ thread, message, references, unresolved, created, draftId }`

API: `POST /api/v1/drafts/:id/send`

#### `draft delete` (write scope)

Throw a draft away. Requires --yes.

```
sharedoc draft delete <id> [flags]
```

Arguments:

- `id` (required) — draft id

Flags:

- `--yes — required`

`--json` shape: `{ ok, deleted, set }`

API: `DELETE /api/v1/drafts/:id`

#### `draft set show`

A variant set: its versions, and — once the human has chosen — which one won and whether they edited it. --wait blocks until they decide.

```
sharedoc draft set show <set-id> [flags]
```

Arguments:

- `set-id` (required) — set id from `draft create` or `drafts list --sets`

Flags:

- `--wait <string> — block until it is chosen or dismissed (default 10m, 0 = no limit); exit 0 chosen · 4 dismissed/failed · 7 still open`

`--json` shape: `{ set:{id,status,note,chosenVariantId,chosenLabel,chosenEdited,messageId,resolvedAt,variants}, variants }`

API: `GET /api/v1/draft-sets/:id?wait=`

Examples:

```bash
sharedoc draft set show 51c0…
sharedoc draft set show 51c0… --wait 30m --json
```

#### `draft set dismiss` (write scope)

Close a variant set without sending anything. Requires --yes.

```
sharedoc draft set dismiss <set-id> [flags]
```

Arguments:

- `set-id` (required) — set id

Flags:

- `--yes — required`

`--json` shape: `{ set }`

API: `POST /api/v1/draft-sets/:id/dismiss`

#### `draft variant add` (write scope)

Add a version to an open set (six at most). The human sees it appear.

```
sharedoc draft variant add <set-id> [flags]
```

Arguments:

- `set-id` (required) — set id

Flags:

- `--body <string> — the text (or --body-file / stdin)`
- `--body-file <string> — read it from a file, or - for stdin`
- `--label <string> — what makes this one different: short, formal, with the numbers`

`--json` shape: `{ variant, set }`

API: `POST /api/v1/draft-sets/:id/variants`

#### `draft variant rm` (write scope)

Take a version out of its set (a set left empty is dismissed). Requires --yes.

```
sharedoc draft variant rm <variant-id> [flags]
```

Arguments:

- `variant-id` (required) — variant id

Flags:

- `--yes — required`

`--json` shape: `{ ok, deleted, set }`

API: `DELETE /api/v1/drafts/:id`

#### `draft pick` (write scope)

A person's choice from a terminal: put a version in the composer (the default), --send it, or schedule it with --at / --in. Agents: never pick for the human.

```
sharedoc draft pick <variant-id> [flags]
```

Arguments:

- `variant-id` (required) — variant id from `draft set show`

Flags:

- `--send — send it now, as it is`
- `--at <string> — schedule it`
- `--in <string> — schedule it this far ahead`

`--json` shape: `{ draft, set } | { thread, message, … }`

API: `POST /api/v1/drafts/:id/use · POST …/send · PATCH …{sendAt}`

#### `slack connect` (write scope)

Connect a Slack workspace to your account: opens Settings → Slack's Add to Slack in your browser (it must be signed in to share-doc as you) and waits until Slack hands over access. Choose which conversations come in on that page. Running it again for a connected workspace reconnects it and keeps its links.

```
sharedoc slack connect [flags]
```

Flags:

- `--workspace <string> — the share-doc workspace the Slack conversations go into (default: your first)`
- `--no-browser — print the page to open instead of opening it`
- `--timeout <number> — seconds to wait [default: 300]`
- `--dry-run — print the page and stop`

`--json` shape: `{ connection:{id,team:{id,name,icon},slackUser:{id,handle,avatar},scopes,status,links,…}, created }`

API: `GET /api/v1/slack/connections (then the browser: /settings/slack/connect → Slack → /api/v1/slack/oauth/callback)`

Examples:

```bash
sharedoc slack connect
sharedoc slack connect --workspace brightmotion
```

#### `slack list`

Your Slack connections: the Slack workspace, your Slack handle, how many conversations are linked, status (ok; revoked: reconnect; disconnected), when the last event arrived. Never a token.

```
sharedoc slack list
```

`--json` shape: `{ connections:[{id,workspaceId,team:{id,name,icon},slackUser:{id,handle,avatar},scopes,status,readSync,autoLinkDms,lastEventAt,lastPostAt,lastError,links,createdAt,updatedAt}] }`

API: `GET /api/v1/slack/connections`

Examples:

```bash
sharedoc slack list
sharedoc slack list --json
```

#### `slack links` (write scope)

The Slack conversations a connection brings in: name, read only or two-way, health, how far their history has come in, the Slack id and the share-doc conversation id. With --set <conversation> --direction: `both` makes it two-way (what you post goes to Slack as you), `in` read only, `off` removes the link (its conversation here is archived with its history; nothing changes in Slack). A conversation not linked yet is found in Slack by id or name and linked with --backfill days of history. Everything not named stays as it is.

```
sharedoc slack links [connection] [flags]
```

Arguments:

- `connection` — connection id or Slack workspace name (default: your only one)

Flags:

- `--set <string> — the conversation to change: Slack id (C…, G…, D…) or name (#design, Colin Wu)`
- `--direction <string> — with --set (in | both | off)`
- `--backfill <number> — days of history for a link --set makes or brings back: 0, 7 or 30 [default: 30]`
- `--all — include removed links`
- `--dry-run — say what --set would do`

`--json` shape: `{ connection, team, links:[{id,channel,kind,name,conversationId,direction:in|both,addedBy,status,slackReadTs,lastTs,backfill:{from,until,doneAt},lastError,createdAt,archivedAt}] } | with --set: { changes:[{channel,name,change:added|restored|removed|direction|failed,…}], links }`

API: `GET /api/v1/slack/connections/:id [?all=1]; with --set: GET …/conversations, PUT /api/v1/slack/connections/:id/links {set:{<channel>:in|both|off}, backfillDays}`

Examples:

```bash
sharedoc slack links
sharedoc slack links Brightmotion --json
sharedoc slack links --set "#design" --direction in
sharedoc slack links --set C0123ABCD --direction both --backfill 7
```

#### `slack status`

How your Slack connections are doing: per connection its status, last event, last post, last error and switches; per linked conversation read only or two-way, health, how far its history has come in, its newest message and your Slack read cursor.

```
sharedoc slack status [connection]
```

Arguments:

- `connection` — connection id or Slack workspace name (default: all of them)

`--json` shape: `{ connections:[{ connection:{…}, links:[{…as slack links}] }] }`

API: `GET /api/v1/slack/connections, then GET /api/v1/slack/connections/:id per connection`

Examples:

```bash
sharedoc slack status
sharedoc slack status Brightmotion --json
```

#### `slack file`

The files on a message that came from Slack (they stay in Slack; the hub fetches them for you, with the same access as the message). Without --index: list them, numbered from 1, with a `gone` flag for one Slack no longer serves. With --index n: download file n to --out, or to its own name in the working directory (refused if that exists, unless --force). Message ids and their slackFiles are on `thread show --json`.

```
sharedoc slack file <message-id> [flags]
```

Arguments:

- `message-id` (required) — a message id from `thread show --json`

Flags:

- `--index <number> — download this file (1-based, as listed)`
- `--out <string> — write it here (overwrites); default: the file's name in the working directory`
- `--force — overwrite an existing file of that name in the working directory`
- `--dry-run — say where it would go; download nothing`

`--json` shape: `{ files:[{id,name,mime,size,width,height,kind:image|video|pdf|file,url,thumbUrl,permalink,gone}] } | with --index: { path, bytes, name, mime }`

API: `GET /api/v1/slack/files?message=<id>, then GET /api/v1/slack/files/:id`

Examples:

```bash
sharedoc slack file 5f0c…
sharedoc slack file 5f0c… --json
sharedoc slack file 5f0c… --index 1 --out /tmp/shot.png
```

#### `slack people`

People in a connected Slack workspace you can start a DM with (from share-doc's copy of its member list; bots and deactivated accounts left out). Matches the name, @handle or email; share-doc teammates are marked and listed first. Use a handle or the Slack id with `slack dm`.

```
sharedoc slack people [query] [flags]
```

Arguments:

- `query` — part of a name, handle or email

Flags:

- `--team <string> — which Slack workspace (name or id), when you have more than one`
- `--limit <number> — at most this many [default: 50]`

`--json` shape: `{ connection, team, people:[{id,name,handle,avatar,teammate:{id,name}|null}] }`

API: `GET /api/v1/slack/connections/:id/people?q=`

Examples:

```bash
sharedoc slack people colin
sharedoc slack people --team Brightmotion --json
```

#### `slack dm` (write scope)

Start a Slack DM (one person) or group DM (two to eight) from share-doc, as you, and bring it in as a private share-doc conversation; one that is already linked just opens, one you removed comes back with its history. Nothing is sent to Slack until you post in it (`channel post <conversation-id>`).

```
sharedoc slack dm <person> [flags]
```

Arguments:

- `person` (required) — one or more: @handle, name, or Slack id (U…) — see `slack people`

Flags:

- `--team <string> — which Slack workspace (name or id), when you have more than one`
- `--dry-run — resolve the people and stop`

`--json` shape: `{ change:existing|restored|added, conversationId, workspace, name, link }`

API: `POST /api/v1/slack/connections/:id/dm {users}`

Examples:

```bash
sharedoc slack dm @colin
sharedoc slack dm @colin @dana --json
```

#### `slack disconnect` (write scope)

Disconnect a Slack workspace: Slack revokes the token and share-doc deletes it, and the conversations it brought in are archived with their history (their composers close). Connecting the same Slack account again lets you tick them back in. Nothing changes in Slack. Requires --yes; --dry-run lists what would be archived. --forget then removes a disconnected connection from your list (its archived conversations stay).

```
sharedoc slack disconnect [connection] [flags]
```

Arguments:

- `connection` — connection id or Slack workspace name (default: your only one)

Flags:

- `--yes — required`
- `--forget — remove it from your list (disconnects it first if needed)`
- `--dry-run — list what would be archived`

`--json` shape: `{ connection, archived:[link], slackRevoked, slackError } | { forgotten }`

API: `DELETE /api/v1/slack/connections/:id [?forget=1]`

Examples:

```bash
sharedoc slack disconnect Brightmotion --dry-run
sharedoc slack disconnect Brightmotion --yes
```

### Search & events

#### `search`

Full-text search over messages, doc comments and file contents, fuzzy over paths, folders, channels and people — everything you can open, shared folders included. `in:<project>` covers its docs, its channel, and any post labelled with it. Filters ride in the query: in:#channel|project|dm, from:me|email|name|agent, is:unread, is:archived (archived docs only; excluded otherwise), has:file, before:/after:2026-09-01|7d, and for files only status:<key> (repeat = any of them), assignee:me|email|name, is:unassigned, has:status (a search for other things with these says so in query.notes rather than widening). `--types` picks any combination (files,folders,messages,channels,people) and, with no query words, browses what is recent in scope; `--workspace all` searches every workspace you can see; `--cursor` continues one group.

```
sharedoc search [query] [flags]
```

Arguments:

- `query` — words, "exact phrases", -exclusions, plus filters like in:#general from:me (optional with --types)

Flags:

- `--scope <string> — one group, or all (the older spelling; not with --types) (all | files | messages | threads | channels | people) [default: all]`
- `--types <string> — comma list of content types: files, folders, messages, channels, people (default: all)`
- `--source <string> — messages from chat (channels, DMs) or document comments (all | chat | comments) [default: all]`
- `--by-thread — group matching messages by thread (one row per thread, best match shown)`
- `--folder <string> — only under this folder of --project`
- `--conversation <string> — only this conversation (id)`
- `--in <string> — sugar for in: — #channel, a project slug, or dm`
- `--from <string> — sugar for from: — me, an email, a name, or agent`
- `--unread — sugar for is:unread`
- `--archived — sugar for is:archived — search archived docs instead of live ones`
- `--has-file — sugar for has:file (messages that tag a file)`
- `--after <string> — sugar for after: — 2026-09-01 or 7d`
- `--before <string> — sugar for before:`

`--json` shape: `{ query:{text,filters,unresolved[],conflicts[],fullText,notes[]}, scope, types[], filters:{messageSource,presentation,project,folder,conversation}, groups:{files[],folders[],messages[],threads[],channels[],people[]}, counts (shown, not totals), page:{<group>:{shown,hasMore,cursor}}, coverage:{mode,workspaces[]}, results[] }`

API: `GET /api/v1/search`

Examples:

```bash
sharedoc search 'contract deadline' --json
sharedoc search 'status:blocked assignee:me' --types files
sharedoc search 'deploy' --in '#general' --after 7d
sharedoc search 'invoice' --scope files
sharedoc search 'is:unread from:emily' --scope messages
sharedoc search 'sync' --types files,folders --workspace all
sharedoc search 'budget' --types messages --source comments --project sharedoc
sharedoc search --types files --project sharedoc --folder specs --limit 20
```

#### `events tail`

The change log (who did what, when) from a cursor; --follow streams.

```
sharedoc events tail [flags]
```

Flags:

- `--cursor <number> — start after this event id (default: last 50)`
- `--kinds <string> — comma-separated kinds, e.g. doc.updated,member.added`
- `--follow — keep streaming (SSE); output becomes one event per line (NDJSON with --json)`

`--json` shape: `{ events:[{id,kind,workspace,project,actor,viaAgent,subject,payload,createdAt}], nextCursor }`

API: `GET /api/v1/events`

#### `unread`

Your notifications (mentions, DMs, replies in threads you follow, new threads on docs you follow). Unread only by default.

```
sharedoc unread [flags]
```

Flags:

- `--all — include what you have already read`
- `--kind <string> — one kind (mention | dm | reply | thread | channel | invite | system | reminder)`
- `--watch — print what is unread, then keep streaming new ones (SSE) until Ctrl-C`

`--json` shape: `{ notifications:[{id,kind,title,snippet,threadId,conversationId,conversationName,projectSlug,docPath,actor,viaAgent,readAt,createdAt}], unread, mentions, nextCursor }`

API: `GET /api/v1/notifications`

Examples:

```bash
sharedoc unread
sharedoc unread --watch --json
```

#### `unread read` (write scope)

Mark notifications read by id.

```
sharedoc unread read <id...>
```

Arguments:

- `id` (required) — ids from `unread`

`--json` shape: `{ read:n }`

API: `POST /api/v1/notifications/read`

#### `unread ack` (write scope)

Mark everything read (needs --yes), or everything in one conversation / before a time.

```
sharedoc unread ack [flags]
```

Flags:

- `--conversation <string> — only this channel / DM`
- `--before <string> — only notifications up to this ISO time`
- `--yes — required for a blanket ack`

`--json` shape: `{ read:n }`

API: `POST /api/v1/notifications/read`

#### `notify prefs` (write scope)

Show or change your notification preferences (spec §3.8): level, thread replies, doc follows, email timing, snooze, working hours.

```
sharedoc notify prefs [flags]
```

Flags:

- `--set <string> — key=value[,key=value] — keys: email browser pushAlways level threadReplies docFollows inboxDrops invites emailMode awayMinutes digestHour markRead snoozeUntil (ISO or null) hoursDays (every|weekdays) hoursStart hoursEnd (minutes from midnight) timezone (IANA)`

`--json` shape: `{ prefs:{email,browser,pushAlways,level,threadReplies,docFollows,inboxDrops,invites,emailMode,awayMinutes,digestHour,markRead,snoozeUntil,hoursDays,hoursStart,hoursEnd,timezone} }`

API: `GET|PUT /api/v1/notify/prefs`

Examples:

```bash
sharedoc notify prefs
sharedoc notify prefs --set level=all,emailMode=never
sharedoc notify prefs --set snoozeUntil=2026-09-04T17:00:00Z
sharedoc notify prefs --set hoursDays=weekdays,hoursStart=540,hoursEnd=1080
```

### Sync

#### `connect` (write scope)

Two-way sync a local directory with a project (long-running by default).

```
sharedoc connect <project> <dir> [flags]
```

Arguments:

- `project` (required) — project slug
- `dir` (required) — local folder (empty folder = pulls everything). Dotfiles, node_modules, lockfiles, __pycache__/, build output at the top (dist/, build/, out/, target/, coverage/) and what the folder's .sharedocignore lists are never added; .gitignore is not read

Flags:

- `--once — reconcile once and exit — pull remote, push local, print a summary`
- `--reset — move the folder aside into .share-doc/backup-<ts>/ and re-seed from the hub (never deletes)`
- `--strict — overlapping edits are a conflict, not a merge: nothing is applied, --once exits 4`
- `--no-comments — do not write <doc>.comments.md next to docs that have open threads (on by default; the sidecar is read-only and never pushed)`
- `--force — go ahead on a folder the sync set already covers, or on a git repository root (pair docs/ instead — a root pair uploads README and source)`
- `--yes — first sync of a folder past 50 files or 20 MB: skip the warning, and send it at once even in a workspace with large files turned off (which otherwise holds it for approval)`
- `--folder <string> — map the local folder to this folder inside the project instead of the whole project (a shared folder's recipient can connect just that folder)`
- `--sync-default <string> — what this connection syncs when no folder rule says otherwise (v0.40) (inherit | on | off) [default: inherit]`
- `--dry-run — say what would be mapped: no registration, no scan, no transfer`

`--json` shape: `{ pulled, pushed, merged, conflicts }   (--once only)`

API: `POST /api/v1/connections (register) · GET …/policy · PUT /api/v1/files/content (push) · Hocuspocus + GET /api/v1/files (pull)`

Examples:

```bash
sharedoc connect homa-legal ~/deals/homa
sharedoc connect homa-legal ./homa --once --json
sharedoc connect homa-legal ./homa --once --strict   # exit 4 on a conflict
sharedoc connect homa-legal ./homa --reset
```

#### `sync add` (write scope)

Add a folder ⇄ project pair, or channels to mirror, to this machine's sync set (~/.config/share-doc/sync.json).

```
sharedoc sync add [project] [dir] [flags]
```

Arguments:

- `project` — project slug or name, any case (with <dir>)
- `dir` — local folder to keep in sync with it

Flags:

- `--strict — this pair: overlapping edits are a conflict, not a merge`
- `--folder <string> — map the folder to this folder inside the project (default: the whole project)`
- `--sync-default <string> — the connection's default: inherit (use folder defaults), on (all unless excluded), off (none until selected) (inherit | on | off) [default: inherit]`
- `--force — pair a git repository root anyway (refused otherwise — pair its docs/ folder)`
- `--yes — first sync past 50 files or 20 MB: skip the warning, and send it at once even in a workspace with large files turned off (which otherwise holds it for approval)`
- `--channel <string> — mirror these channels, comma-separated: general,ops`
- `--channels — mirror every channel you can see (project channels included)`
- `--dms — mirror DMs and group DMs too (off unless asked)`
- `--dir <string> — root folder for the channel mirror [default: ~/sharedoc/chat]`
- `--history <string> — how far back to backfill: 30d, 12h, all [default: 30d]`
- `--workspace <string> — the workspace this pair or the mirror runs in — stored in sync.json; needed when the saved login is one token that sees several workspaces`

`--json` shape: `{ pair:{project,dir,strict,workspace?}, path, firstPush?:{files,bytes,ignored} } | { channels:{dir,include,exclude,dms,history,workspace?}, path }`

Examples:

```bash
sharedoc sync add homa-legal ~/deals/homa
sharedoc sync add ops ~/ops --strict
sharedoc sync add --channel general,homa-legal
sharedoc sync add --channels --dms --dir ~/chat
sharedoc sync add --channels --workspace brightmotion   # a login with no workspace on it
```

#### `sync rm` (write scope)

Remove a folder pair (<dir>), one channel (#name) or the whole channel mirror (channels) from the set, and disconnect the pair's connection on the hub. Never touches files — local or on the server: the way out for a paired folder that went `missing` when you meant to stop syncing it.

```
sharedoc sync rm <what>
```

Arguments:

- `what` (required) — <dir> | #channel | channels

`--json` shape: `{ removed, path }`

Examples:

```bash
sharedoc sync rm ~/deals/homa
sharedoc sync rm #random
sharedoc sync rm channels
```

#### `sync archive-missing` (write scope)

A paired folder is gone (`missing` in `sync status`) and you did mean to delete everything in it: archive on the server every file its connection syncs, as one batch (`sync undo` reopens it), then unpair it as `sync rm` does. The running sync never reads a vanished folder as deletes. Requires --yes; refuses a folder that is still there.

```
sharedoc sync archive-missing <dir> [flags]
```

Arguments:

- `dir` (required) — the paired folder, as in `sync list`

Flags:

- `--yes — required`
- `--dry-run — list what it would archive`

`--json` shape: `{ project, dir, batch, archived:[path], failed:[{path,error}], disconnect } | { dryRun:true, project, dir, archive:[path] }`

Examples:

```bash
sharedoc sync archive-missing ~/old-notes --dry-run
sharedoc sync archive-missing ~/old-notes --yes
```

#### `sync list`

The sync set as configured: pairs, channel rules, resolved paths.

```
sharedoc sync list
```

`--json` shape: `{ projects:[{project,dir,strict}], channels:{dir,include,exclude,dms,history}|null, path }`

#### `sync status`

The set as it is right now: per pair state/files/last push/pull/error, the channel mirror, whether `sync run` is up (and the service manager's view: launchd on macOS, systemd on Linux).

```
sharedoc sync status
```

`--json` shape: `{ running:{pid,mode,since}|null, service:{manager,installed,loaded,running,pid}|null, pairs:[{project,dir,state,files,lastPushAt,lastPullAt,lastError,counts,held,heldDeletes:{batch,since,count,paths}|null,approve}], channels:{dir,state,conversations,threads,lastEventAt,lastError}|null }   (state `missing`: the folder is gone — nothing syncs or is archived until it is back)`

#### `sync run` (write scope)

THE process: every pair and the channel mirror, live, until Ctrl-C (one lock per machine). --once reconciles everything and exits.

```
sharedoc sync run [flags]
```

Flags:

- `--once — reconcile every pair, backfill channels, print one table, exit (4 if a strict pair conflicted, 6 if a pair could not run)`
- `--strict — treat every pair as strict for this run`
- `--service — service mode: under launchd, timestamps and a log at ~/Library/Logs/sharedoc/sync.log (rotated at 5 MB); under systemd, plain lines to the journal`

`--json` shape: `{ pairs:[{project,dir,pulled,pushed,merged,conflicts,error}], channels:{dir,conversations,threads,error}|null }   (--once only)`

Examples:

```bash
sharedoc sync run
sharedoc sync run --once --json
```

#### `sync install` (write scope)

Run `sync run` as a user service — launchd on macOS, systemd --user on Linux — that starts at login and is restarted if it dies. Idempotent. Waits (up to 20 s) for every pair and the channel mirror to report live or an error before printing; `settled: false` means check `sync status` in a moment.

```
sharedoc sync install
```

`--json` shape: `{ unit, program:{argv,via}, service:{manager,installed,loaded,running,pid}, log, journal, linger, settled, pairs:[{project,dir,state,lastError}], channels:{dir,state,conversations,lastError}|null, gated:[dir] }`

Examples:

```bash
sharedoc sync install
sharedoc sync status
```

#### `sync uninstall` (write scope)

Stop and remove the user service (launchd / systemd). The set and every folder stay.

```
sharedoc sync uninstall
```

`--json` shape: `{ removed, unit }`

#### `sync logs`

Tail the service log: ~/Library/Logs/sharedoc/sync.log on macOS, the journal (journalctl --user -u sharedoc-sync) on Linux.

```
sharedoc sync logs [flags]
```

Flags:

- `--follow — keep following, like tail -f`
- `--lines <number> — how many lines [default: 200]`
- `--which <string> — macOS: sync (ours) or launchd (what launchd captured before the logger was up); the journal holds both on Linux (sync | launchd)`

Examples:

```bash
sharedoc sync logs
sharedoc sync logs --follow
```

#### `sync endpoints`

The hub's inventory of machine connections ("connections" in Settings): status, last seen vs last synced, whether settings are applied. Owners see their local paths; project managers a coarse row per machine. `sync list` is this machine's local set.

```
sharedoc sync endpoints [flags]
```

Flags:

- `--project <string> — one project's connections (default: the connected folder's project, else every connection you own)`
- `--all — every connection you own across projects`

`--json` shape: `{ project?, canManage?, syncRevision?, connections:[{id,project,device,owner,own,root,mode,defaultMode,state,status,live,lastSeenAt,lastSuccessAt,configRevision,appliedRevision,applied,dir?,host?,lastError?,rules?,thisMachine}] }`

API: `GET /api/v1/connections?project=`

Examples:

```bash
sharedoc sync endpoints --project homa-legal
sharedoc sync endpoints --all --json
```

#### `sync pause` (write scope)

Pause a connection: transfers stop, folder rules are kept, local edits wait. The running sync notices within 30 s.

```
sharedoc sync pause [connection-id] [flags]
```

Arguments:

- `connection-id` — from `sync endpoints` (default: the connected folder's own connection)

Flags:

- `--dry-run — preview`

`--json` shape: `{ connection, before }`

API: `PATCH /api/v1/connections/:id {state:paused}`

#### `sync approve` (write scope)

Approve what a connection is waiting on. --deletes: the files its folder deleted in bulk (more than 25 at once), held rather than archived on the server — this archives them. Otherwise, in a workspace with large files turned off (workspace settings → Storage; on, the default, nothing waits): its big first push (a `sync add` past 50 files or 20 MB without --yes), or with --file / --held the big files it holds over the workspace's ask-first size. Only on the person's say-so — `sync status` and the log say what is waiting.

```
sharedoc sync approve [connection-id] [flags]
```

Arguments:

- `connection-id` — from `sync endpoints`, or the paired folder's path (default: the connected folder's own connection)

Flags:

- `--deletes — the bulk delete it is holding`
- `--file <string> — a held file's project path (repeatable)`
- `--held — every file it is holding`
- `--dry-run — preview`

`--json` shape: `{ connection, before, approved } | --deletes: { connection, before, decision, heldDeletes:{batch,since,count,paths} }`

API: `POST /api/v1/connections/:id/approve`

#### `sync discard` (write scope)

Keep the files a connection's folder deleted in bulk and is holding: nothing is archived, and the folder pulls them back from the server. (Putting the files back does the same.)

```
sharedoc sync discard [connection-id] [flags]
```

Arguments:

- `connection-id` — from `sync endpoints`, or the paired folder's path

Flags:

- `--deletes — required: the held deletes are what is discarded`
- `--dry-run — preview`

`--json` shape: `{ connection, before, decision, heldDeletes }`

API: `POST /api/v1/connections/:id/discard`

#### `sync undo` (write scope)

Reopen every file one burst of a folder's deletes archived on the server, in one call — by default the newest burst with anything still archived (last 30 days). --list shows the bursts. The folder, if still paired, pulls them back.

```
sharedoc sync undo [connection-id] [flags]
```

Arguments:

- `connection-id` — from `sync endpoints`, or the paired folder's path

Flags:

- `--batch <string> — which burst (from --list, or the log's "undo with" line)`
- `--list — the bursts, newest first`
- `--dry-run — list what it would reopen`

`--json` shape: `{ batch, reopened:[path], skipped:[{path,reason}] } | --list: { connection, batches:[{batch,count,stillArchived,firstAt,lastAt}] }`

API: `POST /api/v1/connections/:id/undo · GET /api/v1/connections/:id/archives`

Examples:

```bash
sharedoc sync undo 3f2a9c1e
sharedoc sync undo ~/kid-learn/docs --list
```

#### `sync resume` (write scope)

Resume a paused connection: it re-reads its settings and reconciles.

```
sharedoc sync resume [connection-id] [flags]
```

Arguments:

- `connection-id` — from `sync endpoints`

Flags:

- `--dry-run — preview`

`--json` shape: `{ connection, before }`

API: `PATCH /api/v1/connections/:id {state:active}`

#### `sync disconnect` (write scope)

Disconnect a connection on the hub: its credential is revoked and any running sync stops; local files, cloud files and membership stay. Requires --yes. Reconnecting is a new `sync add` / `connect`.

```
sharedoc sync disconnect [connection-id] [flags]
```

Arguments:

- `connection-id` — from `sync endpoints`

Flags:

- `--yes — required`
- `--dry-run — preview what is kept`

`--json` shape: `{ connection, before } | { dryRun:true, before, after, keeps }`

API: `POST /api/v1/connections/:id/disconnect`

#### `sync folders` (write scope)

What one connection syncs, folder by folder: list the tree with each folder's rule, effective state and why; `set <path> --mode on|off|inherit` changes a rule (a narrower On beats an ancestor Off); `--default` sets the connection default; `--clear-children <path>` removes overrides under a folder.

```
sharedoc sync folders [connection-id] [set <path>] [flags]
```

Arguments:

- `connection-id` — from `sync endpoints` (default: the connected folder's)
- `set <path>` — the folder to change (project-relative)

Flags:

- `--mode <string> — with `set`: on | off | inherit (inherit removes the rule) (on | off | inherit)`
- `--default <string> — the connection default: inherit = use folder defaults · on = all folders unless excluded · off = no folders until selected (inherit | on | off)`
- `--clear-children <string> — remove every explicit rule strictly under this folder`
- `--if-match <string> — config revision the change is based on (a stale revision is a conflict, exit 4)`
- `--dry-run — preview the impact (files, bytes) without saving`

`--json` shape: `{ connection, policy, rules, folders:[{path,rule,projectDefault,effective:{selected,source,at},mixed,counts}], impact:{selected,selectedBytes,excluded,excludedBytes,total,totalBytes} }`

API: `GET | PUT /api/v1/connections/:id/folder-rules · PATCH /api/v1/connections/:id`

Examples:

```bash
sharedoc sync folders 1a2b3c4d
sharedoc sync folders 1a2b3c4d set media --mode off
sharedoc sync folders 1a2b3c4d set media/previews --mode on
sharedoc sync folders 1a2b3c4d --default off --dry-run
```

#### `folders settings` (write scope)

A project's folder sync defaults (what connections that use folder defaults do with each folder, future folders included). Without --sync-default: list. The root is on|off; other folders may inherit.

```
sharedoc folders settings [path] [flags]
```

Arguments:

- `path` — project-relative folder ("" or omitted = the root)

Flags:

- `--project <string> — project slug (default: the connected folder's)`
- `--sync-default <string> — set this folder's default (on | off | inherit)`
- `--dry-run — preview`

`--json` shape: `{ project, canManage, syncRevision, defaults:{path:mode}, folders:[…], impact }`

API: `GET | PUT /api/v1/projects/:slug/sync-defaults`

Examples:

```bash
sharedoc folders settings --project homa-legal
sharedoc folders settings media --project homa-legal --sync-default off
```

#### `devices list`

Your devices (one per ShareDoc installation) with their connections, grouped by machine.

```
sharedoc devices list
```

`--json` shape: `{ devices:[{id,label,platform,clientVersion,lastSeenAt,connections:[…]}], unmanaged:[…] }`

API: `GET /api/v1/devices`

#### `devices rename` (write scope)

Rename a device's label; its identity and connections are unchanged.

```
sharedoc devices rename <device-id> <label>
```

Arguments:

- `device-id` (required) — from `devices list`
- `label` (required) — new label

`--json` shape: `{ device:{id,label} }`

API: `PATCH /api/v1/devices/:id`

#### `devices disconnect` (write scope)

Disconnect every connection a device holds for you (never erases its disk). Requires --yes; --dry-run lists the affected projects first.

```
sharedoc devices disconnect <device-id> [flags]
```

Arguments:

- `device-id` (required) — from `devices list`

Flags:

- `--yes — required`
- `--dry-run — list what would be disconnected`

`--json` shape: `{ device, disconnected:[{id,project,dir}], count }`

API: `POST /api/v1/devices/:id/disconnect`

### Self-description

#### `help`

This help. --json dumps the full command table for agents.

```
sharedoc help [flags]
```

Flags:

- `--json — machine-readable`

#### `schema`

The live data contract: entities, fields, enums, and the command table.

```
sharedoc schema [entity]
```

Arguments:

- `entity` — one entity, e.g. events, documents, tokens

API: `GET /api/v1/schema`

#### `version`

Print the CLI version.

```
sharedoc version
```

#### `telemetry`

Turn usage reporting on or off, or show whether it is on. After each command the CLI tells the server it is logged in to the command's name, the names of the flags given (never their values or any arguments), the exit code, the duration, the CLI version, OS and architecture. Nothing about files, paths, projects or messages. Also off with SHAREDOC_TELEMETRY=0 or DO_NOT_TRACK=1.

```
sharedoc telemetry [on|off]
```

Arguments:

- `on|off` — turn it on or off; omit to show the current state

`--json` shape: `{ enabled, decidedBy, file? }`

Examples:

```bash
sharedoc telemetry
sharedoc telemetry off
SHAREDOC_TELEMETRY=0 sharedoc files list acme
```

#### `update` (write scope)

Update this CLI in place: runs the installer from the server it targets, then restarts the sync service.

```
sharedoc update [flags]
```

Flags:

- `--no-restart — leave the sync service (launchd / systemd) running the old bundle`

`--json` shape: `{ server, installer, bundle, running?, changed, before:{path,exists,size,sha256}, after:{…}, service:{manager,installed,loaded,running,pid,restarted,error}|null }`

Examples:

```bash
sharedoc update
sharedoc update --dry-run
```


## 6. Exit codes

| Code | Name | Meaning |
|---|---|---|
| 0 | `ok` | success |
| 1 | `usage` | bad arguments/flags |
| 2 | `auth` | not logged in, token revoked, or write scope required |
| 3 | `not_found` | workspace/project/file/thread not found |
| 4 | `conflict` | overlapping edits under --strict, or a stale --if-version (412) — nothing was written; re-read and retry |
| 5 | `rate_limited` | slow down; retry after the hinted seconds |
| 6 | `server` | server or network error |
| 7 | `timeout` | --wait ran out with the draft set still open — ask again later |

With `--json`, the same failure is `{ "error": { "code", "message", "hint" } }` on stdout.

## 7. Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| exit 2, `missing bearer token` | no credential reached the server | `sharedoc login`, or set `SHAREDOC_TOKEN` for headless runs |
| exit 2, `invalid or revoked token` | the token was revoked in `/settings/tokens` | `sharedoc login --force` |
| exit 2, `write scope required — mint a write token` | your token is read-only | re-login with `--scope write` |
| exit 3, `not found` | wrong slug, or you are not a member | `sharedoc projects list --json` and use the exact `slug` |
| exit 4, conflict | someone else wrote the file first | re-read with `files cat`, then write again |
| exit 5, rate limited | too many calls | wait the hinted seconds; batch your reads |
| exit 6, server/network | hub down or `--host` wrong | check `sharedoc whoami --json` → `api` |
| `SHAREDOC_HTTP … doesn't match …` | env host ≠ the host the token was minted for | unset `SHAREDOC_HTTP`, or `sharedoc login --host <url>` |
| `⚠ big first push` on stderr | a folder that never synced had more than 50 new files or 20 MB — it is being sent | next time look before pairing; what should stay local goes in `<dir>/.sharedocignore` (gitignore syntax) |
| `⏸ waiting for approval` (`approval` in `--json`) | the workspace has large files turned off and holds a big first push | show the human the list; `sharedoc sync approve <id>` only on their say-so |

## 8. A project's `sharedoc-instructions.md`

Every project is born with `sharedoc-instructions.md` — its manual: what lives where, how to
connect a machine, the repo's `.gitignore` and `CLAUDE.md` lines, and a **"Where this project
is synced"** table. It is the first thing to read when you enter a project you have not worked in
this session, and it is yours to keep true:

```bash
sharedoc files cat <project> sharedoc-instructions.md           # read it first
sharedoc files put <project> sharedoc-instructions.md --from … --base <version> --message "record pairing: <repo>/docs" --json
```

- **After you pair a folder** — `sharedoc sync add <project> <dir>` or `sharedoc connect` — add a
  row to the table: the repo (or machine), the folder *relative to the clone* (`docs/`), the exact
  command, who and when. Then the next person to clone the repo runs the same command you did.
- **After you change a pairing** (a folder moves, a repo is renamed, a pair is removed) fix the row.
  A stale row costs someone a root connect that uploads their lockfiles.
- **When a repo joins**, make it agree: `/docs/` in `.gitignore` and the "docs/ is synced, not
  committed" section in its `CLAUDE.md` / `AGENTS.md` — the text is in the file, adapt the slug.
- Write with `--base`, keep the rest of the document as it was, and say what you changed in one
  line: in `#<project>` if the project has its own channel, else in `#general` with the label
  (`sharedoc channel post general --project <project>`). A project can be made without a channel —
  then its posts are labels on `#general` and the chat mirror has no `<project>/` folder; the
  project's `sharedoc-instructions.md` says which case it is in.
- If the file is missing (a project older than v0.31), create it from the template at
  `https://sharedoc.brightmotion.io/docs/quickstart` — the sections above — before recording the pairing.

**Making a project and letting people in (v0.33).** `sharedoc projects create <name> --workspace
<slug> --json` makes a project (it opens on its manual). `sharedoc share <dir> --workspace <slug>
--json` does the whole thing for a folder: project, first push, and a join link in `url` — hand
that URL to the human; anyone who opens it joins with the link's role (default editor; `--role
viewer` for read-only, `--expires 30d|never`). `sharedoc invite link --project <slug>` mints
a link for an existing project, `invite links` / `invite revoke <id>` manage them, and
`sharedoc members invite <email…> --project <slug>` sends email invites. Under `--json`,
`--workspace` is required when the human belongs to several. Never create a link with a longer
life or a wider role than the human asked for, and say in the reply that the link admits anyone who
has it.

## 9. What not to do

- **Do not bulk-post.** One comment or thread that says the whole thing beats ten.
- **Do not pass `--yes` to a destructive command** (revoke, remove, delete) unless the human
  asked for that exact removal in this conversation.
- **Look at a folder before its first push.** `connect`, `sync add` and `share` send it straight
  away; past 50 files or 20 MB, show the human what is there and trim it with a `.sharedocignore`
  first. `sync approve` a held push only on their say-so.
- **Do not paste tokens** into files, commits, threads, or your own output. A token you see in the
  environment stays in the environment.
- **Do not edit files in a connected folder without reading them first** — the folder is a replica,
  and someone may be typing in the same doc right now.
- **Do not claim success you have not verified.** Re-read the file or thread first.
- **Do not resolve someone else's thread** unless you were asked to.

---

Short version for `AGENTS.md`: https://sharedoc.brightmotion.io/agents.md · Setting share-doc up: https://sharedoc.brightmotion.io/setup.md · HTML reference: https://sharedoc.brightmotion.io/docs/cli
