Command Shape
- Prefer one logical command per tool call. Separate unrelated inspection, mutation, and verification so failures and approvals remain attributable.
- Do not assume a directory change persists between tool calls. Use the client's
workdir/working-directory parameter or the command's own directory option, such asgit -C <path> status. - Avoid
bash -c, conditional shell programs, compound pipelines, and command substitutions when the agent can run a command, inspect its result, and branch in reasoning instead. - Never start an interactive CLI flow from automation. Pass explicit titles, bodies, messages, and other required inputs.
Background Commands
- Prefer the foreground. Background execution is for a command that must keep running while the agent does other work — a dev server, a desktop app, a file watcher. A slow-but-finite command (a build, a test suite, a formatter) belongs in the foreground with a raised timeout instead; backgrounding one only to skip the wait creates a shell nobody is accountable for.
- Stop every background shell you start. Before a workflow reports its result, review the shells it started and stop each one still running, using the client's own background-shell control (in Claude Code, the kill/stop tool that takes the shell id returned when the shell was started; in Codex, the recorded process). A command that looks finished is not proof its shell exited — check, then stop.
- A leaked shell is not free. Claude Code reports still-running background shells on the
session's
Stopevent, and cenci-watch deliberately holds such a session atrunningrather thandone(#698/#699) because it cannot tell an abandoned shell apart from work the session is genuinely waiting on. The hold clears on the session's next event, and an abandoned shell never produces one, so the session reads as still-working until it ends. - A process meant to outlive the session is exempt, but only when it detaches for real —
its own session/process group and its own log file, never the agent's shell.
cenci babysitis the reference case: it detaches its own supervisor, so its launching shell exits normally and there is nothing to reap.
Search and File Operations
Use the client's native read, search, glob, and patch tools when available. In Codex,
prefer rg/rg --files for repository search when no structured search tool is
available. In Claude Code, prefer Read, Grep, and Glob.
Do not add shell banners such as echo "=== files ===" to batched searches. They
make output noisier and can change approval matching.
Use the client's patch or edit tool for source changes. Shell-based generation is appropriate only for mechanical output, formatter/codegen output, or tools whose normal interface writes files.
Body Files and Heredocs
Heredocs can require shell-created temporary files and may fail in restricted
sandboxes. Write long issue or pull-request content with the client's file tool, then
pass it through a CLI body-file option. Scope every such filename with a run-unique
suffix — <scope> below is a ticket id, PR number, or run id, whichever is already
in scope for the calling skill — so concurrent runs never collide on a shared fixed
name. Any scope key that isn't already format-constrained by its own source (a
numeric ticket/PR ID is inherently safe) must be validated against
^[A-Za-z0-9._-]+$, additionally rejecting ., .., or any value containing ..,
before use — the same rule implement's slug validation applies, since a scope key
can end up as a standalone path segment (e.g. a per-run subdirectory) where a
dot-only value could traverse:
gh issue edit <number> --body-file "${TMPDIR:-/tmp}/cenci/issue-body-<scope>.md"
Any write that also sets a title goes through gh api ... --input instead of
gh issue edit --title/gh issue create --title — neither has a --title-file
equivalent, so a title carried as a shell argument is always an interpolation risk.
Compose the JSON payload mechanically instead of hand-escaping it, in three steps:
The file tool writes the raw title and body as plain text to
<scope>-suffixed files — never a hand-escaped JSON literal.A standalone
jq -n --rawfile title <f> --rawfile body <f>call builds the payload on one source line and redirects it to a payload file. General form:jq -n --rawfile title <f> --rawfile body <f> '{title: ($title | rtrimstr("\n")), body: $body}' > <payload>.json. Concretely:jq -n --rawfile title "${TMPDIR:-/tmp}/cenci/issue-title-<scope>.txt" --rawfile body "${TMPDIR:-/tmp}/cenci/issue-body-<scope>.md" '{title: ($title | rtrimstr("\n")), body: $body}' > "${TMPDIR:-/tmp}/cenci/issue-<scope>.json"rtrimstr("\n")strips the file tool's trailing newline from the title — an untrimmed title breaks the post-write title re-fetch comparison.jqcannot let--rawfilecontent influence the payload's structure, so a well-formed injection attempt in the title/body still can't add or change JSON keys.A separate
gh api ... --input <payload>.jsoncall sends it:gh api repos/<owner>/<repo>/issues/<number> -X PATCH --input "${TMPDIR:-/tmp}/cenci/issue-<scope>.json"
As with the body file above, the raw title/body files and the payload file are all run-scoped by the existing <scope> rule above. The jq call and the gh api call are separate Bash calls (no pipe, no &&) — each is a single non-compound command whose result the agent inspects before proceeding.
gh pr create --title "<title>" --body-file "${TMPDIR:-/tmp}/cenci/pr-body-<scope>.md"
gh pr create has no --input/--title-file equivalent, so its title stays a
plain --title argument (out of scope for the jq pattern above).
Never print or interpolate authentication tokens into command output.
Reading PR CI Status
Never derive a PR's CI status from check conclusions. gh pr view --json statusCheckRollup leaves conclusion empty for a check that is still queued or
running, so a conclusion-only scan silently reads "still running" as "passed" — the whole
class of false-green PR reports (#900).
Read the buckets instead, as one standalone call:
gh pr checks <pr-number> --repo <owner>/<repo> --json bucket,name,state
bucket is the settled classification: pass, fail, pending, skipping, or
cancel. Classify strictly, matching what cenci babysit enforces before it will
automerge (watch/internal/babysit/automerge.go), so an agent's answer and the
supervisor's behavior can never disagree:
- Green only when at least one check exists and every check is in
pass. - Any check in
pending→ the answer is "still running", not green. Report those checks by name and say what is still outstanding; never round them up to a pass. fail,cancel,skipping, or an empty/unrecognized bucket → not green, each reported as itself rather than folded into "failing".- Zero checks is unknown, never green — a PR whose workflows have not been created yet looks identical to a PR with no CI at all.
gh pr checks exits 8 while checks are pending and still writes valid JSON to stdout.
Treat a non-zero exit as a genuine command failure only when the JSON does not decode — an
exit code alone is neither a CI-failure signal nor a reason to report the read as broken.
State the same distinction in prose: "CI is green" means every check finished and passed.
While anything is pending, say so — "CI green except <check>, still running" — even when
every finished check passed.
Shell Portability
Commands may run through zsh, bash, or another configured login shell. Prefer
portable command syntax and avoid bash-only arrays, mapfile/readarray,
BASH_REMATCH, and shell-specific array slicing. When complex shell logic is truly
needed, put it in a reviewed repository script with an explicit interpreter.
Worktrees and Cross-Directory Writes
- Keep the main worktree read-only for implementation and direct edits to the feature worktree with an absolute path or the tool's working-directory option.
- Do not combine
cd <other-dir>with a write, redirection, or unrelated command. - The same absolute-path rule applies to Bash redirect (
>,>>) andteewrite targets specifically:guard-main-worktree.shrefuses every relative target the tokenizer extracts, because a preceding directory change, a subdirectory hook cwd, or a symlink can move where the write actually lands. Use an absolute feature-worktree, plan, design, or temp path so the guard can canonicalize it before deciding; unsupported zero-parse constructs retain the bounded residual documented indocs/adapter-contract.md. - Never rescue an edit made in the wrong worktree with stash, checkout, or file copying. Reapply the intended edit to the correct absolute path, then restore only changes created by the current operation.
Client-Specific Approval Notes
Claude Code
Claude Code allow rules and its shell analyzer inspect every segment of compound
commands. A call containing both cd and a redirection/write can hard-prompt even
when each command is allow-listed. Use standalone calls or git -C.
Codex
Codex evaluates shell segments independently at control operators and may require
approval for any unmatched segment. Supply workdir on each shell call rather than
relying on a previous cd, and request narrowly scoped escalation only after a
sandbox-related failure or when the operation inherently needs it.