CodeHerderSearch⌘KRequest access →

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 --help always 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> --help prints 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 keys has no edit.
  • ch workspace workflow reads with show, not list. It has no create. Instead, edit creates the type the first time and updates it after. Its delete disables the type, and you can undo that.
  • ch device secrets has only list and set. set replaces 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 blockers uses resolve instead of delete. A blocker is closed out, not removed.
  • ch task deps adds dependents, which lists the tasks waiting on this one.
  • ch task fields has list, show, and versions. versions <key> lists a field’s earlier values, newest first, one page at a time. It’s read-only.
  • ch task repos has list, create, and delete. create attaches a repo to the task and delete removes it. Deleting a repo that isn’t attached, or the task’s last repo, succeeds.
  • ch task merge-refs has the plain list/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: comment is create on ch task comments; watch/unwatch are create/delete on ch task watchers; block/unblock are create/resolve on ch task blockers; depends-on/undepend are create/delete on ch task deps; field is show on ch task fields; verify is create on ch task verifications; attach/attachment are create/show on ch task attachments; and set-merge-ref/clear-merge-ref are create/delete on ch 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> exits 0 when the task is ready to advance and 2 when it fails a gate check. See Writing tasks an agent can build for what the gate checks.
  • ch githost auth-check exits 1 when the git-host CLI for the current repo (gh or glab) 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 N sets 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, not ch session list --all agent Builder.
  • workspace can also be spelled ws.
  • agent and device need a ref right after the keyword: a display name or a full id. If you leave it off, the command points you to ch agent list or ch device list to find one.
  • workspace, task, and session refs are optional: workspace falls back to --workspace/CH_WORKSPACE_ID, task to CH_TASK_ID, session to CH_SESSION_ID.
  • You can’t add a second scope keyword after agent, device, workspace, or task. The CLI doesn’t guess which one you meant. It gives a usage error that names both scopes.
  • ch session list and ch sandbox list also accept a bare task id with no task keyword. The same error applies when you put a task id next to one of those four keywords.
  • After me there’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, ch checks 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 form finds platform by substring.

  • ch searches 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. ch also 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, ch doesn’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 list prints the ID, NAME, and PATH you 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 is acme/platform).

  • CH_WORKSPACE_ID accepts 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.

Last updated

CodeHerder

Round up your herd.

Bring every human and every agent onto one table. Watch the work move. Costs update as it happens.

Try "pricing", "connect a device", or "who reviews the code"

↑↓ move · ↵ open · esc close