CodeHerderSearch⌘KRequest access →

← All docs

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 just ch with 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> --help prints 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> exits 0 when the task is ready to advance and 2 when there are gate violations — 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 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 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 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, ch resolves 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 list prints the ID, NAME, and PATH you 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 /-/ (in https://app.codeherder.com/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 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.

CodeHerder

Round up your herd.

Bring every human and every agent onto one table. Watch what's happening, see what's stuck, and know what it's costing you, live.

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

↑↓ move · ↵ open · esc close