Using the ch CLI
The conventions every ch command follows, so the CLI reads the same everywhere.
The ch CLI is workspace-scoped, and every command follows the same pattern. Once you’ve used a couple of verbs, you already know the shape of the rest. This page covers the rules that hold across the whole CLI, so you learn them once.
Command structure
Every ch invocation follows the same pattern:
ch [global flags] <verb> <subcommand> [args]
A verb is a noun like task, device, or webhook; a subcommand is the action on it, like list, show, or create. Global flags, covered below, can go anywhere on the line.
Finding your way around
You don’t need a reference open in another tab. Help is built into three levels:
ch --helpalways prints every verb at a glance.ch <verb> --help(or the bare verb, e.g.ch task) prints that verb’s subcommands plus a worked example.ch <verb> <subcommand> --helpprints the detail for that one subcommand: its arguments, flags, and defaults.
Bare ch, with no verb at all, is different. In a real terminal on macOS or Linux it opens an interactive session console. See Your sessions in the terminal. It prints the same list as ch --help only when it can’t open the console. That happens when output is piped or redirected, when --json is set, or on a host other than macOS or Linux.
If you mistype a subcommand, the CLI tells you it doesn’t recognize it.
Short-form aliases
Three subcommands are common enough to get a short form. The short form works on every verb that has that subcommand:
| Full form | Short form | Example |
|---|---|---|
list |
ls |
ch task ls |
show |
get |
ch device get <id> |
delete |
rm |
ch webhook rm <id> |
A short form works only where the full subcommand exists. A repo, for example, has no delete. You retire it with archive/restore instead, so ch repo rm isn’t recognized either.
A few verbs add their own extra alias on top of a specific subcommand: ch repo restore is also ch repo unarchive, and ch webhook event-types is also ch webhook catalog. These belong to that one verb only. They aren’t part of the ls/get/rm set.
A few verbs spell the write op differently, and edit, set and update all name that same op. A verb accepts whichever one it defines, and the other two run it. So ch variable edit is ch variable set, and ch task update is ch task edit. Use the spelling in the verb’s --help.
Changing a record’s own fields: edit
To change a record’s own fields, you always use ch <thing> edit. It works on task, workspace, schedule, webhook, inbound-endpoint, repo, agent, team, device, member, stage, survey, and skill. Run ch <thing> edit --help to see which fields a record accepts. The fields differ from one kind of record to the next, but the verb stays the same. ch stage edit and ch skill edit are the exceptions. ch stage edit takes one JSON document with only the parts you change. The stage keeps every part you leave out. ch skill edit <slug> --from-dir <dir> replaces the skill’s whole file set with the folder’s content. Neither takes one flag per field, so their --help doesn’t list separate fields.
Removing a record: delete is permanent, archive/restore is reversible
ch <thing> delete (short form rm, see Short-form aliases above) means an irreversible removal. It works on workspace, schedule, webhook, inbound-endpoint, and skill (see Skills). Run ch <thing> delete --help for the specifics, or see that entity’s own guide.
For skill, schedule, webhook, and inbound-endpoint, you can’t undo a delete. ch workspace delete is permanent too. It runs only on a workspace you’ve already archived, so archiving is the confirmation step.
ch device delete and ch agent delete are retired. They used to archive the record rather than remove it, so delete meant different things on different records. Use ch device archive or ch agent archive instead. If you type the retired word, the CLI prints Did you mean: ch device archive ? (or ch agent archive ?) instead of failing silently. The full list of reversible commands: ch device archive (and restore), ch agent archive (and restore), ch repo archive (and restore, alias unarchive), ch workspace archive (and restore, the reversible step before the permanent delete above), and ch wiki archive (and restore). ch member keys delete <hash> reads similarly but only revokes one API key; it doesn’t remove the member. See Managing a record’s child collections below.
Three more verbs answer to delete/rm without removing anything permanently. Each one is another name for a different command. ch msg delete is the same command as ch msg archive: it hides a message you sent, and ch msg restore brings it back. ch variable delete is the same command as ch variable unset: it clears that scope’s own row for a key, nothing more. ch stage delete is the same command as ch stage disable. You can undo it with ch stage enable. The CLI refuses it while a workflow still uses that stage. Typing rm on any of the three runs the reversible command, so check which one you’re running before you rely on the short form.
Managing a record’s child collections
Some records own a whole collection of child records. Examples are a team’s members, a member’s API keys, and a device’s workspace links. A task owns its comments, watchers, blockers, dependencies, fields, verifications, attachments, repos, and merge refs. To reach one of these, use ch <parent> <child> <subcommand>. The subcommands are the same list/create/edit/delete as everywhere else, one level deeper:
ch team members create <team> <member> --role admin
The tables below show the ones you’re most likely to use. They don’t list every collection. A workspace’s quality-review queues and workflow proposals work differently, and their own pages cover them. Seven belong to a record other than a task:
| Command | What it holds | Bare ref lists? |
|---|---|---|
ch team members |
A team’s membership (see Teams) | Yes |
ch member keys |
A member’s API keys (see Minting API keys) | Yes |
ch device workspaces |
The workspaces a device is linked to (see Sharing a device across workspaces for more) | Yes |
ch device secrets |
Which keyring keys on a device its owner has approved a session to use (see Secrets on a device) | Yes |
ch agent config |
An agent’s launch configs (see Agents and the CLI) | No, a subcommand is always required |
ch inbound-endpoint rules |
An inbound webhook’s routing rules (see Inbound webhooks) | No, a subcommand is always required |
ch workspace workflow |
A workspace’s workflow schemas (see Customising workflows) | No, a subcommand is always required |
Nine more all belong to a task:
| Command | What it holds | Bare ref lists? |
|---|---|---|
ch task comments |
The task’s comment thread (see Collaborating) | Yes, and inside an agent session you can drop the task too |
ch task watchers |
Who’s subscribed to the task (see Watching tasks and notifications) | Yes, and inside an agent session you can drop the task too |
ch task blockers |
The task’s blockers (see Blockers and blocked tasks) | Yes, and inside an agent session you can drop the task too |
ch task deps |
The task’s dependency graph (see Task dependencies for more) | Yes, and inside an agent session you can drop the task too |
ch task fields |
The task’s declared field values (see Editing and cancelling tasks) | Yes, and inside an agent session you can drop the task too |
ch task verifications |
The task’s recorded verification results (see How work flows) | Yes, and inside an agent session you can drop the task too |
ch task attachments |
The task’s attachments (see Attaching files and images) | Yes, and inside an agent session you can drop the task too |
ch task repos |
The repos a task works in (see Seeing which repos a task works in) | Yes, and inside an agent session you can drop the task too |
ch task merge-refs |
The task’s recorded merge requests, one per repo (see How the work lands) | Yes, and inside an agent session you can drop the task too |
Not every collection has every subcommand:
ch member keyshas noedit.ch workspace workflowreads withshow, notlist. It has nocreate. Instead,editcreates the type the first time and updates it after. Itsdeletedisables the type, and you can undo that.ch device secretshas onlylistandset.setreplaces the device’s whole list in one call. It doesn’t add one key. So pass every key you still want, not just the new one.
A task’s own nine differ too:
ch task blockersusesresolveinstead ofdelete. A blocker is closed out, not removed.ch task depsaddsdependents, which lists the tasks waiting on this one.ch task fieldshaslist,show, andversions.versions <key>lists a field’s earlier values, newest first, one page at a time. It’s read-only.ch task reposhaslist,create, anddelete.createattaches a repo to the task anddeleteremoves it. Deleting a repo that isn’t attached, or the task’s last repo, succeeds.ch task merge-refshas the plainlist/create/delete.
To change a field’s value, use ch task edit <taskId> --<key> "text" (or --<key>-file <path>). Run ch task fields <taskId> to see the field keys the task accepts. ch task fields show --value still writes a field too, but ch task edit is the command to use. See Editing and cancelling tasks.
Run ch <parent> <child> --help to see which subcommands a collection supports.
Most of these also let you drop the subcommand and just give the parent’s ref. That lists the collection. It works on four of the seven non-task rows above (all but agent config, inbound-endpoint rules, and workspace workflow), and on all nine task rows:
ch team members <team>
# same as: ch team members list <team>
The nine task rows go one step further. Inside an agent session the task ref is optional (see Agents and the CLI). So if you drop the ref too, the command lists the current task’s collection:
ch task comments
# same as: ch task comments list <the task CH_TASK_ID points to>
The other three (agent config, inbound-endpoint rules, workspace workflow) always need a subcommand. If you leave it off, you get a usage error.
Some older command names came before this pattern. They still work, so existing scripts keep running:
- On
ch task:commentiscreateonch task comments;watch/unwatcharecreate/deleteonch task watchers;block/unblockarecreate/resolveonch task blockers;depends-on/undependarecreate/deleteonch task deps;fieldisshowonch task fields;verifyiscreateonch task verifications;attach/attachmentarecreate/showonch task attachments; andset-merge-ref/clear-merge-refarecreate/deleteonch task merge-refs.
Use whichever reads better to you.
JSON output and scripting
Add --json to any command to get the raw JSON response instead of the formatted table or summary. This helps when you pipe into jq or another script:
ch task list --status code --json | jq '.data[].id'
--json is a global flag. It works the same way on every verb.
Exit codes are consistent, so a script can branch on success or failure:
- A usage error (bad flags, missing required argument) exits
2. ch task validate <taskId>exits0when the task is ready to advance and2when it fails a gate check. See Writing tasks an agent can build for what the gate checks.ch githost auth-checkexits1when the git-host CLI for the current repo (ghorglab) isn’t authenticated.
Paging through long lists
Most commands that list many records return one page at a time. This includes tasks, comments, messages, blockers, attachments, sandboxes, and activity. Two flags control paging, the same way on every command:
--limit Nsets how many rows come back in this page.--cursor <token>resumes from where an earlier page left off.
Each command has its own default and ceiling:
| Command | Default rows | Max rows |
|---|---|---|
ch task list |
50 | 200 |
ch task list --watching |
50 | 200 |
ch task list --awaiting-approval |
200 | 500 |
ch task children |
50 | 200 |
ch task comments |
50 | 200 |
ch msg inbox |
50 | 200 |
ch sandbox list (task scope) |
50 | 200 |
ch sandbox list workspace |
200 | 500 |
ch session list |
50 | 200 |
ch task blockers |
200 | 500 |
ch task attachments |
200 | 500 |
ch activity (all four scopes) |
50 | 200 |
Leave --limit off for the default; raise it toward the max to see more per call.
How you know there’s more
Table output prints one line to standard error whenever another page is waiting:
more results — re-run with --cursor <token>
The line goes to stderr on purpose. So when you pipe or redirect stdout, the line stays out of your data.
With --json, the same signal is a next_cursor key. It’s present only when there’s another page, and it’s never present but empty. So a script stops when the key is missing, not when the value is empty. Here are two calls, worked by hand:
ch task list --limit 20 --json | jq '.next_cursor'
# a token: more rows exist, so feed it back in
ch task list --limit 20 --cursor <that token> --json | jq '.next_cursor'
# null: the key is absent, so you're at the end
A short page doesn’t mean you’re done
Stop when a page comes back with no cursor, not when it looks short. A few lists, such as ch task list --watching, can return fewer rows than you asked for and still give a cursor. This happens because they filter rows after they set the page boundary. Only a missing cursor tells you that you’ve seen everything.
The one command that pages for you
ch task tree takes no --cursor and needs none. It fetches every level itself, so a task with many children still shows in full. Its only size flag is --depth (default 5). That limits how deep it prints, not how much it fetches.
ch activity also takes --cursor, but it moves forward through history and stops in a different way. See Activity feeds.
Choosing whose data a list shows
Four commands read data that can belong to more than one thing: ch activity, ch costs, ch session list, and ch sandbox list. All four take the same kind of leading keyword to say whose data you want:
| Command | Accepted scopes | Default with no keyword |
|---|---|---|
ch activity |
me, agent <ref>, workspace [<ref>], task [<ref>] |
me |
ch costs |
me, agent <ref>, task [<ref>], session [<ref>], workspace [<ref>] |
me |
ch session list |
me, task [<ref>], agent <ref>, device <ref>, workspace [<ref>] |
task |
ch sandbox list |
task [<ref>], workspace [<ref>] |
task |
The keyword follows the same rules on all four commands:
- It comes first, before any flags. Write
ch session list agent Builder --all, notch session list --all agent Builder. workspacecan also be spelledws.agentanddeviceneed a ref right after the keyword: a display name or a full id. If you leave it off, the command points you toch agent listorch device listto find one.workspace,task, andsessionrefs are optional:workspacefalls back to--workspace/CH_WORKSPACE_ID,tasktoCH_TASK_ID,sessiontoCH_SESSION_ID.- You can’t add a second scope keyword after
agent,device,workspace, ortask. The CLI doesn’t guess which one you meant. It gives a usage error that names both scopes. ch session listandch sandbox listalso accept a bare task id with notaskkeyword. The same error applies when you put a task id next to one of those four keywords.- After
methere’s nothing to combine. An extra keyword there fails as an unrecognized argument.
ch activity workspace # this whole workspace (or pass a ref)
ch costs agent Builder # one agent's spend, by display name
ch session list device build-box-01 # everything that's run on one device
ch sandbox list ws # every sandbox in the workspace ("ws" for "workspace")
Each command’s own flags, such as filters by type, time window, or state, are on its own page: Activity feeds, Understanding costs, Sessions from the command line, and Agents and the CLI.
Listing archived records
Several commands use the same flags to decide whether a list includes archived rows: ch device list, ch repo list, ch workspace list, ch agent list, ch wiki list, and ch msg inbox.
- No flag: archived rows are hidden. This is the default on all of these commands.
--active: the same default, spelled out.--all: every row, active and archived together.--archived: archived rows only.
You can use only one of the three flags. If you pass two, the command fails with a message that names all three:
$ ch device list --all --archived
device list: --active, --all and --archived are mutually exclusive
ch wiki list follows the same rule, with one difference. There, only an admin or owner can use --all and --archived. See Workspace wiki for why. ch wiki folders shows the wiki’s folder tree, not a list of pages. It takes the same three flags, with the same limit.
No other list command takes --archived. ch session list, ch sandbox list, and ch task blockers also accept --active/--all. On these commands the flags mean open or closed, not archived. Open means a session or sandbox that still runs, or a blocker that isn’t resolved.
A task’s own archived status is a separate thing. See Archiving a finished task for that.
Listing enabled and disabled records
Several commands use a second pair of flags to decide whether a list includes disabled rows: ch device list, ch agent list, ch schedule list, ch skill list, ch stage list, ch survey list, ch observability alerts list, ch session subscriptions list, ch task subscriptions list, and ch workspace workflow show.
- No flag: every row comes back, enabled and disabled together. This is the default on all of these commands.
--enabled: enabled rows only.--disabled: disabled rows only.
You can use only one of the two flags. If you pass both, the command fails with a message that names both:
$ ch schedule list --enabled --disabled
schedule list: --enabled and --disabled are mutually exclusive
A narrowed call looks like this:
ch schedule list --enabled --limit 10
ch workspace workflow show reads with show, not list (see Managing a record’s child collections above). It still takes the same pair. ch workspace workflow show --disabled lists only the types you’ve turned off.
These flags only filter a list. To turn a record on or off, use commands such as ch schedule enable/disable, ch skill enable/disable, and ch stage enable/disable. Each record’s own guide covers those.
Supplying text: inline, a file, or stdin
Some flags take a body of text, such as task titles and descriptions, comments, wiki pages, and message bodies. These flags accept text in three ways:
- Inline:
--flag "some text"(or--flag=text). - A file:
--flag-file <path>reads the value from a file. Use this one first. - Stdin:
--flag-file -(or, on some flags, a bare--flag -) reads from standard input. Only one flag per command can read from stdin. If a command takes more than one text flag, pipe stdin into one and give the rest inline or from a file.
Most of these flags also accept the older --flag --from-file <path> form. A command with only one text flag also accepts a standalone --from-file <path>, because it can mean only that flag. You’ll see both forms in older scripts. A command with more than one text flag has no standalone --from-file, because it wouldn’t be clear which flag it’s for. Examples are ch task create (--title and --description) and ch agent config edit (--persona and --system-prompt).
Use the file and stdin forms for anything long or multi-line, such as a comment body, a spec, or acceptance criteria. That way your shell’s quoting doesn’t get in the way. See Writing tasks an agent can build for a worked example using --title and --description.
Referring to things on the command line
Every ch verb needs a way to point at a specific record, such as a task, a device, or an agent. Most kinds of record follow one rule. The exceptions are workspaces, variables and secrets, --created-by, and one flag that takes only a secret’s id.
A full 36-character UUID always works, for any kind of record, and needs no workspace. For some kinds of record it’s the only thing that works. The CLI rejects a shortened prefix:
$ ch task show 019e4d24
task show: "019e4d24" is not a full 36-char UUID — run `ch task list` to find the full id
There’s a reason for this. Ids follow time order, so records made close together share a long prefix. A prefix match could point at the wrong record. Run the verb’s list (or ls) subcommand and copy the full value from the ID column instead. A task, webhook, sandbox, session, blocker, launch config, attachment, or message has no human-readable name, so this UUID-only rule applies to all of those.
For everything else, a name works too. For a wiki page, its slug works. Agents, members, devices, teams, repos, and schedules all have a human-readable name, and a wiki page has a slug. ch searches your current workspace and ignores case. It tries an exact match, then a prefix match, then a substring match. It stops at the first step that finds anything:
ch device show macbook # substring match on the device's name (or hostname, if unnamed)
ch schedule show "Simplify app" # prefix match on the title
| You’re referring to… | The name it matches |
|---|---|
| An agent | Display name |
| A member | Display name |
| A device | Name: the one you set with ch device edit --name, or its hostname if you haven’t set one (see Naming a device) |
| A team | Name |
| A repo | Name |
| A schedule | Title |
| A wiki page | Slug |
An archived wiki page matches only for an admin or owner. For everyone else, a slug lookup searches live pages only. See Workspace wiki for how to find and restore an archived page.
If more than one record matches at the same step, ch doesn’t guess. It fails and lists every match, so you can be more specific:
$ ch device show build-box-
device show: ambiguous device ref "build-box-": 2 matches — be more specific
019f2000-0000-7000-8000-0000000000b1 build-box-01
019f2000-0000-7000-8000-0000000000b2 build-box-02
Matching a name needs a workspace to search within. Without one, ch tells you so instead of guessing:
$ ch device show macbook
device show: "macbook" is not a full 36-char UUID and no workspace is set to resolve a name by — set --workspace/CH_WORKSPACE_ID, or pass the full id from `ch device list`
Repos follow this same name-or-UUID rule, with one difference. A repo’s full UUID works even with no workspace set.
Variables and secrets are different. You name one by its exact key, such as DEPLOY_TOKEN, plus an optional scope (device <ref> or agent <ref>). There is no prefix or substring match, and no id lookup. Every ch variable command needs a workspace. See Variables.
One flag is the exception: --credential-ref ENV=<secretId> on ch agent config edit/create always takes the secret’s id, never its name. See Secrets for how to give an agent a secret.
Workspaces take a name, slug, slug path, or id. The global --workspace flag, CH_WORKSPACE_ID, and most workspace arguments accept any of these four forms. Examples are ch workspace show <ref>, ch workspace edit <ref>, and both arguments of ch workspace set-parent <child> <parent>. The one exception is the --parent flag on ch workspace create. It takes the new parent’s full id, with no name lookup.
ch task ls --workspace acme/platform
-
Matching uses the same exact → prefix → substring steps as above. At each step,
chchecks the slug path, then the slug, then the name. It moves to the next step only when all three find nothing. So a partial workspace name works too, like a partial device or schedule name.ch task list --workspace formfindsplatformby substring. -
chsearches every workspace you can reach, however many there are. -
A workspace still matches by its old slug path after you move it or its parent.
chalso prints a note to stderr with the new path. So a script that uses the old path keeps working, and you see that the workspace moved:note: workspace ref "acme/old" has moved — canonical path is now "acme/new" -
An archived workspace matches by ref just like an active one.
-
If more than one workspace matches at the step that decided the search,
chdoesn’t guess:$ ch task list --workspace acme task list: ambiguous workspace ref "acme" matches multiple workspaces: acme/platform (019f2000-0000-7000-8000-0000000000b1), acme/payments (019f2000-0000-7000-8000-0000000000b2) -
No match at all looks like this:
$ ch task list --workspace nonesuch task list: no workspace matching "nonesuch" (try `ch workspace list` for names, slugs, paths and ids) -
ch workspace listprints theID,NAME, andPATHyou need. A slug path is the workspace’s parent slugs and its own slug, joined with/. It’s also the part of the web app’s address right before/-/(in{{app_base}}/acme/platform/-/tasks/<id>, the slug path isacme/platform). -
CH_WORKSPACE_IDaccepts all four forms too. Despite the name, it doesn’t have to be a UUID.
For example, ch workspace show acme/platform prints that workspace’s id, name, status, parent, and root.
ch task list --created-by accepts a full UUID, or me for yourself. It rejects a shortened id, the same way the UUID-only kinds above do.
| You’re referring to… | Accepted forms |
|---|---|
| An agent, member, device, team, repo, or schedule | Full UUID, or its name (exact → prefix → substring) |
| A wiki page | Full UUID, or its slug (exact → prefix → substring; live pages only, unless you’re an admin or owner) |
| A variable or secret | Its exact key at one scope (no prefix, substring, or id match) |
| A task, webhook, sandbox, session, blocker, launch config, attachment, or message | Full UUID only |
| A workspace | Name, slug, slug path, or UUID. Any workspace you can reach, including archived or moved ones (--parent on ch workspace create needs the id) |
ch task list --created-by |
Full UUID, or me |
Two more kinds of reference have their own rules, on their own pages. For the task a command uses by default from CH_TASK_ID, see Agents and the CLI. For who a message goes to, see Messages and your inbox.
Identity, workspace, and where they come from
Every command needs to know who you are and which workspace to work in. By default both come from your environment (CH_TOKEN, CH_WORKSPACE_ID). They can also come from a named profile or from a repo’s committed project defaults file. You can override them on one command with --token / --workspace. See Credentials and profiles for the full picture, including which source wins when more than one is set.
In the same way, task commands take their task from CH_TASK_ID by default. See Agents and the CLI for how that works.
One more global flag helps in scripts. --no-retry (or CH_NO_RETRY=1) turns off the automatic retry after a short network error. The CLI retries only commands that are safe to repeat. Leave retries on when you work by hand. Turn them off when you want a script to fail fast on the first network error.
--no-auto-update skips the self-update check that ch start and ch session attach otherwise run first. See Updating the CLI for how that check behaves and how to opt a non-interactive caller into it.
Related guides
- Agents and the CLI — installing the CLI, agent configuration, and the current-task default
- Credentials and profiles — signing in, API keys, profiles, and credential precedence
- Quickstart — install the CLI and ship your first task
- Writing tasks an agent can build — the three ways to supply text, in action
Last updated