Using the ch CLI
The conventions that apply to every ch command — discoverable help, the universal ls/get/rm short forms, JSON output and exit codes for scripting, the three ways to supply text, and how to refer to a task, workspace, repo, or other record.
The ch CLI is workspace-scoped, and every command follows the same grammar. Once you’ve used a couple of verbs, you already know the shape of the rest — this page covers the mechanics that hold across the whole CLI, so you learn them once instead of rediscovering them command by command.
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(or justchwith nothing else) prints the full command surface — 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.
If you mistype a subcommand, the CLI tells you it’s unrecognized rather than silently doing nothing.
Short-form aliases
Three subcommands are common enough across the CLI that they get a universal short form, available 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> |
The short form only resolves where the full subcommand actually exists — for a verb that has no delete (agents, for example, use lifecycle actions like assign/approve instead), 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, ch webhook event-types is also ch webhook catalog, and ch quality first-pass-rate is also ch quality fpr. These are one-off shortcuts on the individual verb, not part of the universal ls/get/rm set.
JSON output and scripting
Add --json to any command to get the raw JSON response instead of the formatted table or summary — useful when you’re piping 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.
For scripts that branch on success or failure, exit codes are consistent:
- A usage error (bad flags, missing required argument) exits
2. ch task validate <taskId>exits0when the task is ready to advance and2when there are gate violations — 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 more than a handful of records — tasks, comments, messages, blockers, sandboxes, activity — hand back one page at a time. Two flags control that, the same way everywhere they appear:
--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 |
50 | 200 |
ch task blockers |
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>
That’s on stderr on purpose, so piping or redirecting stdout leaves it untouched.
With --json, the same signal is a next_cursor key: present only when there’s another page, never present but empty. A script’s stop condition is “the key is missing,” not “the value is empty.” Two calls, worked by hand:
ch task list --limit 20 --json | jq '.next_cursor'
# a token — more rows exist; feed it back in
ch task list --limit 20 --cursor <that token> --json | jq '.next_cursor'
# null — key absent, 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 handful of lists — ch task list --watching among them — can return fewer rows than requested and still hand back a cursor, since rows get filtered out after the page boundary is fixed. Absence of a cursor is the only reliable “you’ve seen everything.”
The one command that pages for you
ch task tree takes no --cursor and needs none — it fetches every level internally, so a task with more children than fit on one page still renders in full. Its only sizing flag is --depth (default 5), limiting how deep it prints, not how much it fetches.
ch activity also takes --cursor, but walks a different, forward-only history with a different stop signal — see Activity feeds.
Supplying text: inline, a file, or stdin
Flags that take a body of text — task titles and descriptions, comments, memory entries, message bodies, and more — all accept the same three forms:
- Inline:
--flag "some text"(or--flag=text). - A file:
--flag --from-file <path>reads the value from a file. - Stdin:
--flag --from-file -(or, on some flags, a bare--flag -) reads the value from standard input.
The file and stdin forms are the practical choice for anything long or multi-line — a comment body, a spec, a set of acceptance criteria — since they avoid fighting your shell’s quoting. 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 — a task, a device, an agent. Most kinds of record follow one rule; workspaces, --created-by, and one credential-only flag are the exceptions.
A full 36-character UUID always works, for any kind of record, and never needs a workspace to be looked up. For a handful of record kinds it’s the only thing that works — a shortened prefix is rejected:
$ ch task show 019e4d24
task show: "019e4d24" is not a full 36-char UUID — run `ch task list` to find the full id
This isn’t arbitrary: ids are assigned in time order, so records created close together share a long common prefix — matching on a prefix would be ambiguous about which record you meant. Run the verb’s list (or ls) subcommand and copy the full ID column value instead. A task, webhook, sandbox, dev session, memory entry, 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. Agents, members, devices, teams, repos, secrets, and schedules all have a human-readable name, and ch matches it for you — within your current workspace, case-insensitively, trying an exact match, then a prefix match, then a substring match, and stopping as soon as one tier 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 rename, or its hostname if you haven’t set one (see Naming a device) |
| A team | Name |
| A repo | Name |
| A secret | Name |
| A schedule | Title |
If more than one record matches within a tier, ch doesn’t guess — it errors and lists every candidate so you can be specific:
$ ch device show build-box-
device show: ambiguous device ref "build-box-": 2 matches — be more specific
019efe7c-9332-7ef2-8e2a-b43821dc0422 build-box-01
019f1c11-6626-7066-8a26-52826a733861 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 and secrets follow this same name-or-UUID rule, with one workspace-scoping wrinkle each: a repo’s full UUID resolves even with no workspace set, while every ch secrets command needs a workspace regardless of whether you pass a name or an id.
One flag is the exception: --credential-ref ENV=<secretId> on ch agent config set/add always takes the secret’s id, never its name — see Secrets for how to wire a secret into an agent.
Workspaces take a name, slug, slug path, or id. The global --workspace flag, CH_WORKSPACE_ID, and most workspace arguments — such as ch workspace show <ref>, ch workspace edit <ref>, and both arguments of ch workspace set-parent <child> <parent> — accept any of these four forms. The one exception is the --parent flag on ch workspace create: it takes the new parent’s full id directly, with no name lookup.
ch task ls --workspace acme/platform
- Matching is case-insensitive.
- If more than one workspace could match,
chresolves in this order: slug path, then slug, then name — the first tier with any match wins. - If more than one workspace matches within that tier, the CLI doesn’t guess: it errors and lists every candidate so you can be specific.
ch workspace listprints theID,NAME, andPATHyou need. A slug path is a workspace’s ancestry joined with/— it’s also the part of the web app’s address bar right before/-/(inhttps://app.codeherder.com/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 the literal me for yourself — a shortened id is rejected the same way the UUID-only kinds above are.
| You’re referring to… | Accepted forms |
|---|---|
| An agent, member, device, team, repo, secret, or schedule | Full UUID, or its name (exact → prefix → substring) |
| A task, webhook, sandbox, dev session, memory entry, blocker, launch config, attachment, or message | Full UUID only |
| A workspace | Name, slug, slug path, or UUID (--parent on ch workspace create needs the id) |
ch task list --created-by |
Full UUID, or me |
Two more references have their own rules worth reading in full rather than repeating here: the task a command defaults to via CH_TASK_ID — see Agents and the CLI — and 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 it’s operating on. Both come from your environment by default (CH_TOKEN, CH_WORKSPACE_ID), can be loaded from a named profile, or overridden per-command with --token / --workspace — see Credentials and profiles for the full picture, including precedence when more than one source is set.
Task-scoped commands similarly default their task argument from CH_TASK_ID — see Agents and the CLI for how that works.
One more global flag worth knowing for scripts: --no-retry (or CH_NO_RETRY=1) disables the CLI’s automatic retry of transient network errors on commands that are safe to retry. Leave retries on for interactive use; disable them if you want a script to fail fast on the first transient error instead of waiting through a retry.
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 triple-form text convention in action
