Start

Octopool CLI

Octopool CLI

The octopool Go CLI is the product entrypoint. It logs in against the relay, stores a caller token locally, and acts as a drop-in gh shim for the read-only GitHub commands Octopool supports. Local GitHub writes and read fallbacks pass through the string rewrite guard before dispatch with your own GitHub credentials.

Source: cmd/octopool/.

The compiled-in default endpoint is https://octopool.dev. No config is required for normal OpenClaw use, and self-hosted servers are selected at login time.

#Install modes

Install with Homebrew:

brew install openclaw/tap/octopool

Or install from source:

go install github.com/openclaw/octopool/cmd/octopool@latest

The binary inspects argv[0] and behaves as a gh shim when invoked as gh or octopool-gh:

  • octopool gh ... — explicit subcommand.
  • symlink octopool as gh — transparent shim on PATH.
  • symlink octopool as octopool-gh — side-by-side shim.

#octopool install-shim [--shell zsh] [--dry-run]

Installs the transparent gh shim for interactive, login, and non-interactive zsh processes. The command:

  • resolves and pins the real GitHub CLI as OCTOPOOL_GH_PATH, preventing fallback loops;
  • creates an isolated gh symlink under $XDG_DATA_HOME/octopool/bin (or ~/.local/share/octopool/bin);
  • adds or replaces a clearly marked managed block in $ZDOTDIR/.zshenv (or ~/.zshenv) so the shim directory stays first on PATH; and
  • verifies the result with non-interactive zsh -c and login zsh -lc processes, failing when a later startup file shadows the shim.

Re-running the command is idempotent. Existing shell configuration outside the managed block is preserved. --dry-run validates the plan and prints its paths without writing. Only zsh is currently supported because it has a startup file that every interactive and non-interactive invocation reads.

Executable discovery follows symlinks and accepts regular octopool files, requiring execute bits on Unix. On Windows it also recognizes octopool.exe case-insensitively without requiring Unix execute bits. This recognition does not establish support for the full zsh installer on native Windows.

octopool install-shim --dry-run
octopool install-shim
zsh -c 'command -v gh'

#Commands

#octopool login [server]

Reads a local GitHub.com token (GH_TOKEN, then GITHUB_TOKEN, then gh auth token --hostname github.com), exchanges it with POST /v1/login/github-cli, and saves the returned caller token. Native lookup always targets GitHub.com regardless of GH_HOST; a missing or empty GitHub.com token fails login without falling back to Enterprise credentials. The server can be passed as a positional argument, --server, or the older --url flag:

octopool login
octopool login https://octopool.your-org.dev
octopool login --server https://octopool.your-org.dev
octopool login --client build-mac
  • If gh auth token fails, the CLI prints the exact real-gh web reauthentication and Octopool retry commands instead of returning only the subprocess exit status.
  • The login URL must be HTTPS. http:// is allowed only for loopback hosts, or when OCTOPOOL_ALLOW_INSECURE_LOGIN=1 is set for local development.
  • The CLI fetches GET /.well-known/octopool, verifies service: "octopool", uses the discovered api_base, and defaults to the discovered default_pool unless --pool is passed.
  • If discovery points api_base at a different host, login fails unless --trust-discovery-redirect is passed. This keeps a mistyped or compromised discovery host from silently receiving your local GitHub token.
  • After discovery, the login exchange follows only same-origin HTTP redirects. Trusting a discovered api_base does not authorize later redirects to other origins.
  • The token is stored 0600 at <user-config-dir>/octopool/auth.json (URL, pool, token, login, client, timestamp).
  • The default client name is the local hostname. --client overrides it; re-login rotates only that named client's caller token and leaves the user's other clients active. All trailing .local suffixes are normalized away, case-insensitively, preserving base case (Host.local.local becomes Host, distinct from host). The default hostname is normalized then truncated to 80 characters; login normalizes that result again. Explicit --client values are never truncated and must satisfy the server's 1-80 character limit after normalization. whoami displays the saved login response; re-login updates an old saved spelling. An ambiguous historical client family returns client_name_ambiguous: ask an admin to review the conflicting credentials. Refusal leaves existing tokens valid.
  • Each caller retains up to 16 named clients. Adding another retires the least recently updated session, which bounds abandoned hostname and ephemeral-runner credentials.
  • Octopool validates the GitHub identity and OpenClaw org membership during login, and binds the caller by immutable GitHub user id. See Auth.
  • Verified members of ALLOWED_GITHUB_ORG self-enroll into DEFAULT_LOGIN_POOL. Admin provisioning remains available for non-default pools and manual token issuance.
octopool login
# logged in to https://octopool.dev as steipete for pool maintainers from steipete-mbp

#octopool whoami [--json]

Prints the saved login target:

octopool whoami
# server: https://octopool.dev
# pool: maintainers
# login: steipete
# client: steipete-mbp

Use --json for scripts.

#octopool gh api <GET path> [--paginate] [--slurp] [--jq <expr>]

Relays a read-only gh api call through Octopool's cache and pool. Prints the GitHub response body exactly like gh api, optionally piping it through jq -r -- <expr>. Check-suite detail and suite check-run lists preserve native raw JSON framing, including no added trailing newline; --jq retains its usual line-oriented output. Incomplete output, including a failed final newline write, returns an error without retrying the read or delegating to native gh. The expression is always a filter, never a jq command-line option. On Windows, relay-backed --jq requires native jq 1.7 or newer and adds --binary to retain native gh LF output terminators and preserve literal string bytes, including embedded LF, CRLF, lone CR, and Unicode. Other platforms omit the Windows option. GET reads using --paginate and --slurp stay relay-cached for up to 10 pages. When present, GitHub's Link header authoritatively selects the next page; header-less web-synthesized and legacy-cached responses fall back to array-length and total_count shape heuristics. Longer result sets and unprovable header-less shapes fall through to the real gh for a complete response.

--hostname github.com and --hostname=github.com keep supported reads on the relay, including under active protection rules and with scalar query fields or pagination. Other explicit hosts are not relayed. Existing protected native commands retain their GitHub.com host pinning. These explicit-host REST reads request max-age=0 by default to preserve their previous live-read behavior. An explicit Cache-Control header controls reuse; unqualified REST reads keep their existing cache policy.

Explicit --method GET calls can pass scalar query parameters with -f/--raw-field or -F/--field. They share cache entries with the equivalent URL query, including under active protection rules. Typed integers use native numeric conversion and typed null becomes an empty query value; raw values stay strings. The normalized outbound query is checked before relay dispatch. Calls without an explicit method retain native POST behavior, apart from the existing workflow-run read exception. Nested/array fields, file inputs, placeholders, duplicate query keys and unsupported shapes retain native handling. Permission-sensitive branch-protection and ruleset reads also retain their existing native query-field dispatch.

octopool gh api repos/openclaw/openclaw/pulls/85341 --jq .number
# 85341
octopool gh api --method GET repos/openclaw/octopool/contents/README.md -f ref=main --jq .sha

Repository protection reads use guarded native fallback, retaining your existing GitHub permissions and exact authenticated response shape. No protection response uses pooled credentials or shared cache, even for a public repository:

octopool gh api repos/openclaw/openclaw/rules/branches/main --jq type
octopool gh api repos/openclaw/openclaw/rulesets --paginate
octopool gh api 'repos/openclaw/openclaw/rulesets/42?includes_parents=true'
octopool gh api repos/openclaw/openclaw/branches/feature%2Ftopic/protection

The exact GET allowlist also covers documented protection subresources. Supply literal owner/repo/branch values; encode a slash within a branch as %2F. Decoded structural policy checks still apply. The Worker returns fallback_local / local_credentials_required after its policy checks, and the CLI fetches authoritative policy again before native dispatch. String-rewrite policy denial or unavailability never permits fallback; OCTOPOOL_NO_FALLBACK=1 rejects the handoff. This is read capability, not a determination that a PR meets or fails a review requirement.

With active rules, these exact reads and their supported output flags use strict API preparation, including GitHub.com host pinning. Safe unmodeled routes or flags retain the best-effort native filtering described below; they do not become modeled protection reads merely because their path is similar.

The exact gh api user --jq .login identity probe first validates the saved caller token against Octopool's pool-health endpoint, then prints the saved login without spending GitHub API or pooled-identity quota. Token/URL overrides, other projections, headers, queries, and full user-profile reads retain the normal relay or real-gh behavior.

#Known landing GraphQL reads

The shim recognizes the exact public PR CI summary, paginated CI detail, and merge snapshot queries used by OpenClaw's landing tools. Their canonical definitions live in GITHUB_LANDING_QUERIES in src/github-public-shapes.ts. Whitespace and optional commas may vary; fields, aliases, arguments, and variable declarations must match. The CLI accepts only their owner, name, typed integer pr/number, and optional detail cursor fields. It translates each read to one canonical shaped PR request; the Worker executes its own fixed GraphQL query with a pooled identity. The response remains GraphQL JSON, including --jq projection and a failing exit status for GraphQL errors.

The Worker keeps these three shapes in a 60-second cache with no outage stale fallback. The CLI requests max-age=0 by default, preserving the independent live observations that existing landing and CI-confirmation tools relied on before pooling. An explicit -H 'Cache-Control: max-age=30' can opt into bounded reuse for advisory reads; mandatory observations retain max-age=0. Each refresh makes one GraphQL resource request through the pool and republishes the response for other readers; independent visibility and caller-authentication checks retain their own lifetimes. Cursor pages have separate cache keys, and callers must continue checking head/snapshot consistency across pages. Cache hits and fresh requests retain authoritative outbound protection, including the logical /graphql endpoint and fixed query text under both server and local rules. Every request admission reloads those rules. Upstream HTTP and GraphQL failures remain errors; they never masquerade as an older Worker and trigger personal-token fallback.

Viewer-dependent merge previews, mutations, file inputs, and unsupported output modes stay with native gh. Other repository queries can use the scoped route below. Private repositories retain guarded local fallback. A CLI connected to an older Worker rejects its raw REST response before any output and uses guarded fallback. The merge projection also requires the source branch name (headRefName); a Worker returning an older merge projection triggers the same handoff before any partial output. Upgrade both components for pooled GraphQL. These exact projections still run before the repository-query recognizer.

#Repository GraphQL reads

Plain gh api graphql -f query=... calls can relay one or more repository-rooted reads using a read-only GitHub App token restricted to that repository. This moves GraphQL quota from the personal token to the App; it does not change the native writer identity. All roots must name the same owner/repository (case-insensitively), with optional aliases and __typename fields. Batches spanning different repositories stay native.

gh api graphql -f owner=openclaw -f name=openclaw -F pr=42 \
  -f 'query=query($owner:String!,$name:String!,$pr:Int!){repository(owner:$owner,name:$name){pullRequest(number:$pr){state mergeable headRefOid}}}' \
  --jq .data.repository.pullRequest

The local token check is only an optimization. The Worker owns the real AST validation, pool owner policy, public visibility, and verified single-repository App-token boundary; see repository GraphQL security. No PAT or installation-wide token can execute an arbitrary query. Nested reads cannot inherit the App's access to unrelated private repositories or organization permissions.

The CLI accepts scalar -f/--raw-field and typed -F/--field variables, optional operationName, literal or variable repository arguments, and --hostname github.com. It preserves --jq and the native nonterminal response bytes, except for the local viewer splice described below. Template and other unsupported presentation flags retain native handling, as on other API relay reads. Input files, pagination, nested/array field flags, duplicate fields, foreign hosts, conditional or credential headers, block strings, and unrecognized grammar stay native. Only default JSON Accept and Cache-Control headers are eligible. rateLimit probes, mutations, and subscriptions stay native.

A single top-level viewer { login } (optionally aliased) can accompany the repository roots. Its selection must be exactly login, without arguments, directives, aliases, or fragments inside it. The CLI reads the active github.com host's user from native gh's hosts.yml (GH_CONFIG_DIR, then XDG_CONFIG_HOME/gh, the Windows AppData location, or ~/.config/gh) and requires an exact match with Octopool's saved login. Token overrides, missing/unreadable config, account mismatches, and ambiguous config stay native. This proof makes no network identity call. The CLI removes the viewer root before relaying, then inserts its login into data in document key order, preserving error arrays and leaving data: null unchanged. The login never enters the shared cache. Other viewer fields or nested viewer uses stay native.

Repository queries default to Cache-Control: max-age=60, including explicit --hostname github.com calls, matching the Worker's graphql_read TTL. Explicit Cache-Control headers retain their requested bound; -H 'Cache-Control: max-age=0' and OCTOPOOL_FRESH=1 force live reads (the environment override takes precedence). Cached responses have a 60-second server ceiling and no outage stale fallback. Full queries and variables partition PR and cursor pages. HTTP-200 GraphQL errors are printed and return failure, without a second native query. A relay fallback, older Worker, transport, or ordinary service failure uses guarded native gh before any output; policy failures, authentication denials, and cancellation stay terminal. OCTOPOOL_NO_FALLBACK=1 refuses the handoff. OCTOPOOL_GRAPHQL_RELAY=0 disables this new recognizer, preserving the existing exact landing projections and their routing.

#Read routing and freshness

--json selects output fields; it does not guarantee a relay read. A bundle containing unsupported fields such as PR reviewDecision or issue comments delegates the entire export to native gh. The shim now explains this on stderr before native execution, after protection checks. Request only fields the consumer needs, but retain required merge, permission and review decisions. Separate descriptive and decision reads only when the consumer can preserve consistency across observations. See the complete field-delegation inventory, including the lossy historical :field journal mapping and retained parity boundaries.

gh api --include (or -i) selects native gh and caller credentials, and now prints a routing notice on stderr. It changes the reader identity and quota, not just response framing or freshness. Use OCTOPOOL_FRESH=1 or Cache-Control: max-age=0 for a supported relay read. Supported pagination and explicit --hostname github.com can still relay.

For final CI decisions, verify the exact run ID, expected head SHA and run attempt using a fresh read or exact-run watcher. Broad and filtered run lists can lag on a fresh upstream acquisition. A fresh MISS returning an older list is not evidence of expired Octopool cache reuse; changing to --include is not a guaranteed fresh-list workaround. Cache HITs can follow successful revalidation and do not measure avoided GitHub requests.

#Quota provenance and merge diagnostics

Native dispatches also enter a local delegation journal, so desktop callers that discard stderr can still be attributed to their parent process.

Native GraphQL delegation prints octopool: graphql delegated to personal token on stderr. This includes unsupported PR exports, native PR checks, and unrecognized gh api graphql queries; relay-backed reads do not print it. When the last observation has fewer than 500 remaining, the notice labels the remaining count and reset as a REST /rate_limit estimate, cached for up to 60 seconds and not a retry deadline. The shim reads this endpoint once through native gh before delegation, using the personal credential's cache. It never substitutes the pool's quota. A failed or policy-blocked quota probe leaves the delegation notice without guessed numbers.

A raw or native GraphQL rate-limit error, including GraphQL errors returned with HTTP 200, retains the original native error and adds guidance to inspect the failed response's headers for retry timing. The diagnostic does not know that response's reset time and does not infer it from the separate REST estimate, which can report a full bucket while GraphQL is exhausted. No second quota probe runs on failure. Subsequent misleading invalid-token/login hints are suppressed; unrelated 403 permission errors, REST failures in mixed commands such as pr diff, and secondary-rate-limit/backoff diagnostics stay unchanged. stdout and native exit codes are preserved, and prompts and watches keep streaming stderr.

Bare gh api rate_limit is noncacheable but can use a pooled reader. After successful relay output, Octopool prints one fixed stderr notice: this is pooled-reader quota, not proof of the native writer's quota or permissions. JSON stdout is unchanged. Errors, failed output, native fallback, and caller-owned --include probes do not get that notice. The saved-login shortcut above also stays quiet; a matching login is not a credential or quota comparison.

For contemporaneous, read-only native evidence, request headers on an actual resource read:

gh api user --include --jq '{login,id}'
gh api graphql --include -f query='query { viewer { login } rateLimit { limit remaining used resetAt } }'

Included-header probes delegate to native gh under the normal outbound policy checks. Inspect the response's resource and quota headers alongside its status and request ID. An adjacent /rate_limit response may disagree with actual REST or GraphQL reads; even a later successful resource read cannot explain an earlier write failure or prove that the earlier writer had quota or permission. These probes perform no merge.

Set exactly OCTOPOOL_DIAGNOSTICS=1 to add one fixed-field merge_diagnostics stderr record at the final gh pr merge dispatch boundary. Other values disable it. It reports the UTC attempt start, elapsed milliseconds (including final preparation and execution), whether the native child started, its outcome and available exit code, and the final route: native or internally converted rest_put. Failed preparation has no dispatched route. A known server_policy_revision belongs to the server; effective_rule_count includes merged local rules, which that revision does not identify. Initial-policy failures before reaching this boundary retain the existing policy-failure diagnostic.

Automation can distinguish a refusal before mutation dispatch using exactly one complete merge_diagnostics record captured from that invocation: child_started=false, outcome=preparation_failed, start_failed, or canceled_before_start, no exit_code, and headers=unavailable establish that the native merge child did not start. The ordinary error can follow this record. This is a dispatch fact, not permission to ignore changed remote state or retry without the caller's normal reconciliation. Missing, truncated, malformed, or duplicate records, an unknown outcome, and child_started=true do not establish non-dispatch. A generic protection error from older clients is insufficient; native children preserve their exit codes and can fail after GitHub accepted a write.

Only an internally converted silent, nonpaginated REST merge can capture response metadata. Octopool checks an added include flag against the same final policy and byte limits, then uses the existing single body preparation and request. If instrumentation cannot pass, the merge arguments remain unchanged and headers=unavailable is reported. Capture suppresses the newly requested raw headers and body, preserving native stderr and the child's outcome. Native merges, generic API calls, caller-owned include output, and pagination are not intercepted. No probe, retry, or extra policy lookup is added.

Accepted metadata is limited to HTTP status, a bounded hex-and-colon GitHub request ID, an allowlisted quota resource (core, search, graphql, integration_manifest, or code_search), and nonnegative signed-64-bit decimal limit, remaining, used, reset, and Retry-After values. Missing fields may be absent. Any malformed selected value, duplicate selected field (including comma-joined values), malformed/truncated framing, extra response or body, line above 4 KiB, or block above 32 KiB invalidates the entire block. Nothing is salvaged. The collector recognizes native gh's tested plain framing and bold-blue header-name wrapper; other terminal escapes are rejected. A future native output-format change may make headers unavailable. Raw output is drained even after rejection.

Added records contain no argv, repository, PR number, SHA, publication text, snapshot path, environment values, rule text, raw errors, credentials, or credential fingerprints. Native stderr remains native output. Diagnostic parse/render/write failures do not change the operation's result. A 403 alone never establishes quota exhaustion, and this observability does not change routing, authentication, permissions, or server behavior or establish the cause of a past failure.

#octopool gh pr|issue|run|repo|release ...

The shim also handles common top-level GitHub CLI read commands by translating them to safe relay routes:

octopool gh pr view 85341 -R openclaw/openclaw --json number,title,url
octopool gh pr view 85341 -R openclaw/openclaw --json number,files
octopool gh pr list -R openclaw/openclaw --state open --limit 20 --json number,title,url
octopool gh pr diff 85341 -R openclaw/openclaw --patch
octopool gh pr diff 85341 -R openclaw/openclaw --name-only
octopool gh pr checks 85341 -R openclaw/openclaw --json name,state,bucket,link,workflow
octopool gh pr checks 85341 -R openclaw/openclaw --watch --fail-fast
octopool gh search issues cache regression -R openclaw/openclaw --state open --json number,title,url
octopool gh search prs rate limit -R openclaw/openclaw --state open --json number,title,url
octopool gh issue view 80490 -R openclaw/openclaw --json number,title,state,url
octopool gh issue list -R openclaw/openclaw --state open --label bug --limit 20 --json number,title,url
octopool gh run list -R openclaw/openclaw --branch main --limit 10 --json databaseId,workflowName,status,conclusion,url
octopool gh run view 26360397003 -R openclaw/openclaw --json databaseId,workflowName,status,conclusion,url
octopool gh run view 26360397003 -R openclaw/openclaw --json status,conclusion,jobs
octopool gh run watch 26360397003 -R openclaw/openclaw --exit-status
octopool gh repo view openclaw/openclaw --json nameWithOwner,defaultBranchRef,url
octopool gh workflow list -R openclaw/octopool --json id,name,path,state
octopool gh workflow view ci.yml -R openclaw/octopool --json id,name,path,state
octopool gh label list -R openclaw/octopool --json name,color,description
octopool gh gist view abc123 --json id,url,isPublic
octopool gh search repos octopool relay --json name,nameWithOwner,url
octopool gh release list -R openclaw/octopool --limit 10 --json tagName,name,url
octopool gh release view v0.3.0 -R openclaw/octopool --json tagName,name,url
octopool gh api repos/openclaw/octopool/contents/README.md?ref=main --jq .content

pr diff NUMBER --name-only parses relayed diff headers like native gh: paths retain diff order, Git quoting and escapes, and renames use the destination path. Output is uncolored even on a terminal, with one newline per name and no output for an empty diff. --name-only takes precedence over --patch and requests diff media. Explicit --color, --web, --exclude, --allow-escape-sequences, and other unsupported modifiers delegate to native gh. Public diffs use the token-free transport.

Bounded CI retries use your local GitHub identity after structural policy checks:

gh run rerun 26360397003 --failed -R openclaw/openclaw
gh api -X POST repos/openclaw/openclaw/actions/runs/26360397003/rerun
gh api --method POST repos/openclaw/openclaw/actions/runs/26360397003/rerun-failed-jobs
gh api -X POST repos/openclaw/openclaw/actions/jobs/123456789/rerun
gh api --method GET repos/openclaw/openclaw/actions/workflows/ci.yml/runs -f event=pull_request -F per_page=100 -F page=2

run rerun ID [--failed] [-R owner/repo] pins one positive decimal run ID and repository; under active protection it becomes the corresponding bodyless REST POST. The three exact REST retry routes accept no body, query, field, pagination, or debug options. Run/job IDs must fit a signed 64-bit integer; zero, leading zeroes, and ambiguous selectors are rejected. The Worker remains read-only. Retries never use pooled credentials.

Workflow-run reads accept literal event, head_sha (40 hex characters), branch, status, page (positive decimal integer), and per_page (1–100) query fields through -f/--raw-field or -F/--field. For this exact route, fields default to GET even without --method GET; they are encoded into the checked query before relay dispatch. Duplicate/unknown fields, typed file sources, placeholders, and request bodies are rejected on this modeled path.

Local repository/branch git probes have a 10-second deadline. A failed probe reports octopool: git unavailable (<resolved path>): <error>; pass -R owner/repo or fix git and exits nonzero, preserving the distinction from a policy denial. Explicit -R or GH_REPO skips repository discovery, so numeric PR/run and repository-scoped reads work without git. Commands that infer or validate a branch still need working git.

Top-level commands relay machine-readable --json shapes and selected non-interactive human-format reads. Supported --json fields are intentionally conservative. Common gh field names are mapped where the REST API uses different names, such as url, author, headRefName, headRefOid, baseRefName, baseRefOid, isDraft, databaseId, workflowName, and nameWithOwner.

Release views can read metadata from public GitHub pages when every requested --json field is in tagName,url,isDraft,isPrerelease,publishedAt:

octopool gh release view -R openclaw/octopool --json tagName,url,publishedAt
octopool gh release view v0.6.9 -R openclaw/octopool --json tagName,isPrerelease

This works for tagged releases and the latest release. --jq applies after selecting the fields, and OCTOPOOL_FRESH=1 still fetches current metadata. Requests containing name, body, or createdAt, as well as release lists, retain exact anonymous API responses. The publication time never substitutes for the creation time, and rendered notes never substitute for raw Markdown. If page metadata is incomplete, Octopool tries the exact anonymous API and keeps the existing guarded fallback behavior. CLI and Worker must both support the metadata shape to use the page; older Workers retain the API path.

gh repo view --json id returns the repository's GraphQL node ID before --jq, matching native gh. Missing, empty, or non-string node IDs use guarded native fallback before any JSON is printed; OCTOPOOL_NO_FALLBACK=1 keeps these cases as failures. Raw gh api repos/OWNER/REPO reads retain the numeric REST id.

Repository views export owner as {id, login} with the owner's GraphQL node ID, and visibility as PUBLIC, PRIVATE, or INTERNAL, matching native gh before --jq runs. Incomplete selected owner or visibility metadata uses the same guarded fallback. Raw API reads retain REST owner fields and lowercase visibility.

With active string protection, modeled pr view, pr diff, and non-watch pr checks also accept checked branch selectors, including feature/topic and owner:feature/topic. These use guarded native gh, not the numeric PR relay routes. Native owns branch lookup (including open/merged/default-branch selection), JSON/jq output, and exit status. Repository and branch material is checked, never rewritten to evade policy; the repository is pinned to GitHub.com and policy is fetched again before native dispatch. Output flag occurrences and the -- delimiter retain their ownership.

An omitted selector resolves the current branch locally, honoring tracked refs/pull/N/head, @{push}, and native push-remote/upstream-branch precedence, including fork-owner labels. Contextual repository resolution honors GH_REPO or one configured gh-resolved default (as set by gh repo set-default) first. Without either, modeled native reads have empty, non-TTY stdin and use native noninteractive remote priority: upstream, then github, then origin, then other names, case-insensitively. A single safe HTTPS GitHub.com origin needs no default-repository setup. All candidates must pass GitHub.com HTTPS validation, and the highest priority must have a unique remote; tied priorities fail closed rather than relying on native's unspecified tie ordering. GH_FORCE_TTY affects output, not stdin, and cannot enable prompting here. No interactive/network repository discovery runs during preparation. Nonempty --repo still requires a selector, as in native gh. Local Git context is converted to explicit checked selectors before dispatch, so later checkout/config changes cannot select another repository or branch. Numeric tracking PRs can retain numeric relay handling; ambiguous unqualified numeric branches fail closed. Detached/missing Git context, PR URL selectors, foreign hosts, SSH repository forms/host aliases, multiple configured defaults or URL/config values, and unsupported ref shapes fail closed on this modeled path. Use an explicit branch with -R OWNER/REPO when local context cannot be proven. SSH remotes remain unsupported because native applies SSH-config URL translation, which this guard does not inspect. Issue/run selector restrictions and PR lifecycle-write rules are unchanged.

gh pr view --json state and gh pr list --json state return the GraphQL lifecycle OPEN, CLOSED, or MERGED regardless of the other requested fields or whether the relay uses a public page or REST. Draft status is separate: an open draft has state: "OPEN" and isDraft: true when requested. This conversion happens before field filtering and --jq, without changing cached payloads, raw gh api REST states, or human-format DRAFT display. gh search prs --json state and gh search issues --json state retain native lowercase open/closed states and return merged when the result has a nonzero nested pull_request.merged_at timestamp. Top-level merged_at and draft status do not change search state. Search conversion also precedes field filtering and --jq and leaves raw gh api /search/issues responses and cached payloads unchanged. gh issue view --json state and gh issue list --json state return native OPEN or CLOSED values across supported field subsets, including REST and public-page/cache payloads. This conversion happens before field filtering and --jq, without changing the cached representation. Raw gh api REST issue states remain lowercase; issue search states and human-format output remain unchanged. Issue views also relay stateReason from a live REST read: completed, not_planned, and reopened become COMPLETED, NOT_PLANNED, and REOPENED; explicit null becomes the native empty string. Missing or unrecognized reasons fall back before output. Issue-view assignees export native id, login, name, and databaseId fields using validated shared profile reads. Issue lists still delegate stateReason; unsupported issue fields such as comments and closed delegate the entire request to real gh. gh pr view --json mergeable likewise returns a JSON enum string before filtering and --jq: REST true becomes "MERGEABLE", false becomes "CONFLICTING", and null or absent values become "UNKNOWN". This uses only REST mergeable, not mergeable_state, draft/lifecycle status, checks, or merge policy. Raw gh api REST reads retain their boolean, null, or absent mergeable values. gh pr view --json mergeCommit reads fresh PR metadata and returns {"oid":"<sha>"} for a merged PR or null for an unmerged PR. An open or closed-unmerged PR's synthetic test merge SHA is never exported as its merged commit. Missing merge status or an invalid merged-commit SHA requests guarded native fallback before printing output. Raw gh api reads retain merge_commit_sha. mergeStateStatus reads the existing pr-merge-snapshot-v1 GraphQL projection live, validates the head against REST, and verifies the head again before output. Its enum is preserved verbatim; REST mergeable_state is not equivalent (for example, native GraphQL BLOCKED versus REST unstable). Missing or unknown enums, incomplete projections and changing heads request guarded native fallback. Head consistency does not make separate check/policy observations an atomic snapshot. autoMergeRequest exports JSON null when REST explicitly supplies auto_merge: null. Non-null or missing auto_merge retains guarded native fallback, as before. Enabled auto-merge exports use caller credentials: pooled credentials may have maintainer permissions, so authorEmail and enabledBy cannot be assumed visible to every caller of a shared relay response. maintainerCanModify exports REST maintainer_can_modify only when it is a JSON boolean; missing, null, or other types use guarded native fallback. mergedBy exports JSON null only when REST explicitly supplies merged_by: null. Non-null actors use the same validated user/bot export and shared profile lookup as author; missing or unrepresentable identities use guarded native fallback. These three fields read live (max-age=0), bypassing public page shapes; incomplete projections fall back before any JSON or --jq output. PR lists and searches do not support these fields. gh pr list --json mergeable and gh pr list --json mergeCommit also delegate. Supported summary-only combinations use the pr-summary-v2 public page shape, including mergeCommit, merged, isDraft, author, and headRepositoryOwner. Login-only page identities use the shared profile lookup for actor types and node IDs; supplied node IDs must still match the profile. Closed-unmerged pages omit draft status, and missing page authors or head owners are omitted. When a requested field needs an omitted value, the CLI retries once through the relay without the shape header, retaining max-age=0 when required. This exact REST retry works with OCTOPOOL_NO_FALLBACK=1 and completes before hydration or output. Adding API-only fields such as mergeable, autoMergeRequest, maintainerCanModify, mergedBy, or headRepository skips the page shape. See the complete supported field set. PR views also relay headRepository, headRepositoryOwner, assignees, and statusCheckRollup to reduce local GitHub quota usage through shared transports and eligible caching, including under active string rewrite protection. Fork metadata uses GitHub node IDs; a deleted head repository is null. Requested author, merger, owner, and assignee names use a shared per-command profile lookup. The rollup preserves native gh CheckRun and StatusContext fields and resolves workflow names by check-suite ID through head-filtered Actions runs and the complete raw workflow catalogue, including inactive workflows. It shares this verified association with pr checks, without using run names, display titles, or body-supplied URLs. Rollup check/status pages and head-filtered Actions runs read live. Workflow names use the shared one-hour catalogue cache; if a verified run's workflow ID has no catalogue name, the complete catalogue is retried once live before refusing the association. Missing run associations do not trigger this retry. OCTOPOOL_FRESH=1 still reads every catalogue page live without an extra catalogue retry. After all hydration, a final live PR head check rejects a moving head before any JSON is printed. A matching SHA is not an atomic snapshot: checks can change on the same commit. Both pr checks and the rollup require consistent page totals, unique upstream IDs, and complete pages, bounded to 1,000 items per collection, including both Actions runs and the workflow catalogue. Missing or ambiguous workflow associations and mismatched check/run heads also refuse projection. Fork head repositories are supported. Inconsistent or incomplete collections emit no partial JSON and follow the existing typed native fallback, including fresh policy checks and host pinning under active rewrite rules. OCTOPOOL_NO_FALLBACK=1 keeps these cases as failures. Repeated JSON fields are accepted and hydrated once. Machine-readable gh pr view --json comments,commits uses fixed public GraphQL projections, including combinations with supported basic fields. Comments paginate in GitHub connection order, up to 1,000 comments; commits relay only when the complete connection fits in 100 entries. Native gh 2.101.0 exports only the first 100 commits and first 100 authors per commit, so larger commit connections retain native handling. Both projections read live (max-age=0) by default, matching the landing projections. The Worker can reuse a projection for at most 60 seconds for explicitly bounded API reads; there is no stale fallback. Totals, PR identity, unique IDs and cursor progression must remain consistent. Commit hydration also verifies the PR head before and after. These checks do not make multiple reads an atomic snapshot.

Comments export native viewerDidAuthor using the active local gh GitHub.com account, never the Octopool login. Two guarded native REST /user reads, before hydration and before output, resolve the active credential to an immutable user ID. These use caller REST quota; the comment queries use pooled GraphQL quota. Author IDs are compared locally and omitted from JSON. Persisted usernames cannot establish authorship after a rename and are never used. Environment-token overrides (GH_TOKEN or GITHUB_TOKEN), unavailable identity reads, changes of account, older Workers, inconsistent pages and bounds use guarded native fallback before any JSON is printed. OCTOPOOL_NO_FALLBACK=1 blocks these typed handoffs. Zero-count reaction groups are omitted exactly as in native gh.

Every request containing reviews still delegates in full, including --json comments,reviews,commits. Native reviews can contain the caller's private pending review, which a viewer-independent pooled projection cannot reproduce. Unsupported grammar retains its existing native routing; raw REST commit/comment/review routes and human rendering are unchanged. See the native export contract for selections, field classification, pagination and the deliberate review exclusion. Upgrade both the Worker and CLI for pooled comment and commit exports; no migration or cache purge is required.

For relayed PR-view exports, author, labels, and files use native JSON projections. User authors export id (node ID), is_bot, login, and name; bot authors use native is_bot and app/… login keys. Labels export node IDs, names, descriptions (null becomes an empty string), and colors in upstream order. Files export only path, additions, deletions, and changeType, including renames. File change types use native enums; REST removed becomes DELETED. Unsupported or missing statuses, including REST unchanged with no native enum, follow guarded fallback before any JSON is printed. When both files and statusCheckRollup are selected, their reads overlap after the initial PR lookup. Both collections finish before the final live head check and JSON output; a terminal policy or authentication failure cancels the sibling read. Upgrade note: PR-view scripts using numeric author/label IDs, REST-only nested keys, lowercase file statuses, or originalPath must use these native projections instead. Upgrade note: scripts relying on earlier Octopool boolean/null output must use explicit enum comparisons. Replace .mergeable == true with .mergeable == "MERGEABLE" and .mergeable == false with .mergeable == "CONFLICTING"; handle "UNKNOWN" separately. Do not rely on truthiness: all three enum strings are truthy in jq. Machine gh run list/view --json exports use canonical, unshaped REST reads through the shared cache, not public-page reconstructions. Human run lists/views and watch retain their page-backed paths. Machine run lists also support --commit/-c, --event/-e, and --created, including attached short values, through the same cache and explicit freshness controls. Creation dates, ranges, and comparison expressions pass unchanged to GitHub's created filter; the CLI does not parse or normalize dates locally. Repeated values use the last occurrence; an empty final value removes that filter. Run lists using these filters without --json retain native gh delegation. The existing 13 list and 15 view fields are unchanged. Run name is the REST run name; workflowName comes only from real workflow metadata, never from name. Missing/null ordinary strings become empty strings, and timestamps use native parsed time defaults. Jobs always have eight keys and steps six, with non-null arrays in acquisition order. Only job/step completion times normalize zero instants; other timestamp offsets and fractional seconds retain native time.Time behavior.

Run views also support jobs: up to 10 canonical returned-attempt pages of 100, preserving GitHub's acquisition order. Every page must report the same valid total, with exact final cardinality, positive unique IDs across pages, and no remaining next link. A short page before completion, inconsistent pagination, or more than 1,000 jobs requires guarded whole-command fallback before any output. Requested nonzero --attempt qualifies the output URL; returned run_attempt owns JSON attempt and the canonical jobs route. Supplied job run IDs and nonempty head SHAs must match that run. Job attempts are not identity evidence: an attempt-qualified response may include successful jobs reused from older attempts after rerunning failed jobs. Historical run identity/head evidence is checked without fetching today's branch head.

When a view selects both jobs and workflowName, their reads overlap after the run is validated. Jobs still paginate in order, and both reads must settle successfully before JSON or jq output. A terminal policy or authentication failure cancels the sibling read.

Only selected workflowName triggers metadata. View uses one verified workflow-ID lookup; filtered lists use one verified numeric/YAML-selector lookup. Nonempty unfiltered lists use the existing complete catalogue (at most 10 pages of 100), then one memoized direct lookup per missing workflow ID. Only a genuine upstream 404 on that last list-specific lookup produces an empty name. Disabled workflows remain valid. Empty lists and unselected names do not fetch metadata. Logical data-operation bounds are 1 for scalar view, 2 with name, 12 with jobs and name, 2 for filtered list with name, and 111 for unfiltered list with name. These exclude policy/transport retries. List responses may not exceed their effective requested limit (default 20, maximum 100); every read retains relay policy and freshness.

Observed native integers outside ±(2^53−1), or unproved identity/collection shapes, use typed unsupported_run_export fallback before JSON or jq output, respecting NO_FALLBACK. The JS service may already have erased numeric or duplicate-key distinctions: this is not a lossless upstream transport guarantee. Malformed native fields and contradictory run identities remain terminal errors, even for unselected modeled fields. Contradictory job ownership uses guarded fallback. Existing attempt, job-total, pagination and catalogue refusal reasons remain distinct. Exports buffer all validation/hydration before output; successful JSON commands return success independently of run conclusion. Downstream writer/jq errors still fail and cannot be rolled back. gh search issues|prs and JSON gh pr list --search / gh issue list --search use repo-scoped, cacheable GET /search/issues reads. Positional search terms and list --search values accept the scoped search grammar, including dates, quoted phrases, non-scope negation, and in:title,body. The client rejects unsupported syntax before relay dispatch. Plain queries can use anonymous search even when pooled search is disabled; richer qualifiers require enabled pooled search and a compatible Worker. Search denial and confirmed rate limits fall back to native gh, unless OCTOPOOL_NO_FALLBACK=1 is set.

List searches support --state open|closed|all (also merged for PRs), author, assignee, and label filters. They preserve native's default open filter and its removal when the search includes a closed/merged clause. List JSON supports number, title, body, state, url, author, updatedAt, closedAt, and labels, plus issue createdAt and PR isDraft/mergedAt. PR createdAt stays native because search returns the issue's creation time, which can differ from the PR's. Authors retain native's is_bot/name semantics via cached user profiles. Top-level search keeps its distinct REST author shape and lowercase states. Its author/assignee/label flags and custom sort/match flags still delegate; the corresponding allowlisted qualifiers can be written in the query.

Both paths request one page, defaulting to 30 results, with GitHub's best-match order unless a sort qualifier is supplied. Limits above 100, human list-search output, empty --search, and unsupported fields such as headRefName, headRefOid, or files delegate. Search results remain cacheable even when exporting state or merge time; OCTOPOOL_FRESH=1 requests fresh search results. Cached bodies avoid a full result download; conditional revalidation can still use GitHub Search quota.

Hydrated gh pr view --json files,... requests send the verified PR head SHA, allowing file pages to share a five-minute state-scoped cache. gh pr checks uses the shared cache throughout ordinary acquisition: its PR head-SHA lookup sends cache-control: max-age=60 so concurrent CI-polling sessions share one upstream PR read at most 60 seconds old, and the check/status reads for that SHA use the normal cache TTLs, as do its raw Actions metadata reads. Workflow names use the shared one-hour catalogue cache, with the same one-shot live catalogue retry for missing names as rollups. Each collection is bounded to 10 pages of 100 entries. Before any catalogue retry, ordinary acquisition uses at most 41 logical data operations, or 21 without Actions associations; an actual empty result takes 3. These are not limits on policy reads, transport attempts, retries, or an entire watch session. A missing-name retry adds at most 10 catalogue operations and does not refetch runs or checks. Every data operation retains authoritative policy checks through the relay client.

Check-run and status collections for the same SHA are acquired concurrently. When validated checks need GitHub Actions metadata, the head-filtered run collection and complete workflow catalogue are then acquired concurrently; requests without Actions checks skip both metadata collections. Pages within each collection still read in order. This also applies to PR rollups and fresh watch confirmation. Successful reads use the same logical operation budget; a failed read can overlap requests from the other bounded collection. A terminal policy, authentication, response-size, or relay failure cancels the other collection, and both settle before output or native fallback. The initiating terminal failure takes precedence over sibling cancellation or fallback; otherwise check-run errors retain precedence over status errors, and run acquisition/identity errors precede catalogue errors. Workflow associations are built only after both metadata reads settle. Result assembly remains check-runs before statuses.

Successful nonempty pr checks --json / --jq exports return 0 regardless of check outcomes; export or writer errors still fail. An empty result fails before any JSON or jq output, with no checks reported on the '<head branch>' branch on stderr and exit 1. Human and watch output instead return 1 for failed checks, otherwise 8 for pending checks, otherwise 0; cancellation and skipping alone do not fail. NEUTRAL maps to skipping. Legacy status descriptions are preserved; check summaries are not descriptions. Legacy statuses and missing/null check timestamps export zero times. Malformed nonempty check timestamps use guarded fallback before output.

Checks are deduplicated before field selection by descending start time, using the native slash-joined check name/workflow/event key and a separate status-context namespace. Equal starts have no promised winner; creation time and numeric ID are not tiebreakers. JSON retains that aggregation order, not human-table presentation order. Native gh run watch and gh pr checks --watch polling also stays on the relay, floors intervals at 30 seconds, and backs off to 120 seconds. Both watches stop immediately if a relay response exceeds the CLI's size limit, including after progress is printed; they do not repeat the download or wait through polling backoff for this failure.

Supported gh run watch commands keep polling on the relay, including under active rewrite rules. Relay failures, including fallback_local for pagination or exhausted pool retries, stop the watch with an error; they never start a personal-token watcher. This applies to the initial read, later polls, completion confirmation, and final job hydration. If a poll returns stale progress, the next scheduled poll requests max-age=0; ordinary caching resumes after a non-stale response, without adding polls or changing the backoff. The terminal poll needs a fresh confirmation unless it is a non-coalesced cache miss or its RFC3339 cache_created_at is at or after watchStart + 2s. watchStart is captured once when the watch begins, before its first poll; the two-second margin allows for client/server clock skew. Earlier in-progress observations and attempt numbers do not prove freshness, since those snapshots can themselves be stale. A confirmation that finds an unfinished run resumes polling.

Jobs use the proven completed run's exact run_attempt. Each page first uses normal caching and is accepted only when every job is completed and every page is either a non-coalesced cache miss or has cache_created_at >= max(run.updated_at, watchStart) + 2s. cache_created_at and run.updated_at must both parse as RFC3339 for the cached proof. Jobs can appear lazily after an older all-completed snapshot, and that snapshot must also postdate the watch's start. Unproven collections are reread with max-age=0. OCTOPOOL_FRESH still forces live reads; older relays without creation timestamps can prove completion only through non-coalesced misses. Missing or inconsistent job metadata fails explicitly without printing a partial job summary or a successful completion message. Job IDs must be positive, unique across all pages, and within the relay's safe-integer range. Supplied run_id and nonempty head_sha must match the owning run; optional ownership fields may be absent from public-page-derived jobs. Human run views and watch collect up to 10 pages of 100 using actions-jobs-v1; the Worker supports later pages through exact REST. Both validate the complete collection before rendering any jobs, including stable totals, full intermediate pages, and consistent pagination links. Human run views retain guarded fallback on invalid data; watch stops without handing off. Octopool preserves the job set returned for that attempt, including reused successes when present, and does not reconstruct missing jobs from earlier attempts. With complete data, --exit-status returns 1 for a non-successful run; without it, a completed run returns 0. Read failures return nonzero with or without --exit-status. Unsupported command shapes still delegate, and an explicit repo_not_public refusal on the initial run lookup retains guarded native fallback. A refusal after watch progress never hands off.

For gh pr checks --watch, an explicit relay fallback_local after progress still prints a handoff boundary and continues the command with real gh, which owns output and exit status. Its first snapshot may repeat the last relay-rendered state. Client-side incompleteness, auth, transport/decode failures (including relay_timeout), and ordinary relay service errors remain terminal after progress and do not spend local GitHub quota. Cached empty checks are revalidated live. Terminal confirmation reacquires every check, status, run, and workflow page with max-age=0, builds fresh associations, then reads the PR head live again after hydration. A changed head cannot confirm completion; a matching head still does not make same-commit check changes atomic. This extra live read is part of each terminal confirmation attempt, not the ordinary 41-operation bound. Active rewrite policy retains the existing pre-handler delegation of checks-watch commands. Ask for raw gh api conditional requests only when instant freshness matters more than quota. --jq runs after --json filtering, matching the usual agent workflow for small machine-readable reads.

#Read-option occurrences and delegation

Modeled read flags use their command's value types. Each --json occurrence is a CSV record: quotes and doubled quotes are decoded, spaces and empty elements are significant, and only the first record is read (with normal CSV CRLF handling). An empty raw value contributes no fields. Repeated occurrences append; valid fields are deduplicated in first-occurrence order before hydration. Malformed or unsupported earlier fields cannot disappear into a partial relay request. Explicit --json= with no effective fields delegates, never selects human output. --json number --json= still selects number. The last --jq program wins, including an empty program; even an empty --jq without --json delegates on top-level commands. API jq needs no JSON flag. Short -q= retains the native literal = value or delegates intact. Already-declared -R, -L, and -q aliases accept attached values such as -Rowner/repo, -L5, and -q.number, including under active rewrite policy; native handoff preserves those spellings. This does not add short-flag clusters.

Issue-list labels use the same CSV occurrence rules. An empty effective sequence omits the labels query key. Decoded labels containing commas, empty elements, or line breaks delegate because the existing REST query cannot preserve them. Spaces and literal quotes remain supported. Ordinary repo, author, assignee, and jq flags are scalars, not CSV. Search repo filters are slices but multiple occurrences or multiple decoded repositories remain native-only. Unsupported filters remain unsupported even when empty. Search label filters retain typed local fallback, including labels that the issue-list REST query cannot represent, so OCTOPOOL_NO_FALLBACK still blocks that native dispatch. API fields/headers stay literal repeated values; workflow run --json stays Boolean. The documented workflow/gist view JSON extensions are unchanged.

Every limit or interval assignment must pass native signed base-0 integer syntax, including prefixes, signs, underscores, and overflow checks. Only the final limit gets the command range check: at least 1 for lists, and 1–1000 for search. Values above the relay's 100-item bound delegate rather than being clamped into eligibility. A final limit outside the platform's native integer representation delegates before conversion or command-range checks. Enum assignments are checked individually, case-insensitively; PR/issue list state is normalized, but run status spelling is preserved and unsupported spellings delegate. Run-view attempts use unsigned 64-bit syntax without signs: zero selects the latest run path, and values beyond the CLI's signed representation delegate before conversion. Jobs still belong to the returned run attempt.

Watch durations must be representable before multiplication and the 30-second floor. An earlier int-valid duration can be overridden; an invalid integer cannot. A final unrepresentable run-watch duration delegates so native lookup and early-completion timing remain native-owned. Delegation changes only the effective valid interval (or inserts a proved default before a real -- terminator), never discarded intervals, another flag's value, or unknown grammar. With no active rewrite rules, native PR-checks watch handoff recognizes -w=false and -w=0 as disabled web mode and keeps the watch floor. The -w spelling remains native-only; short clusters and invalid Boolean assignments are left intact.

Protected native watches apply that floor only after fresh final-policy preparation has inspected the original interval occurrences, retaining any repository already pinned before a relay handoff. Active-policy PR checks-watch still delegates before the handler. Both the input and prepared command must be recognized watches with corresponding option/value, positional, and terminator ownership, apart from checked repository pins. A valid policy rewrite from interval 5 to 40 stays 40; invalid input rewritten into valid grammar, changed commands or option ownership, and unknown grammar retain their policy-prepared argv without a guessed floor. Generated interval values and emitted tokens must also satisfy that final policy; a conflict blocks the child rather than bypassing policy or rewriting the floor below 30 seconds.

These parsers run after the existing policy check. Active rules still inspect original occurrences and structural material, even overwritten scalars or CSV records ignored by decoding. Native argv retains spelling and order except for necessary repository/host pins and the effective watch floor; decoded CSV is never joined back into native arguments. Relay attempts and final native dispatch acquire policy again. The guarded repo view pin is positional, including when resolved from context or supplied with the shim's repo option; native repo view has no --repo flag. Publication duplicate restrictions and typed-fallback OCTOPOOL_NO_FALLBACK behavior are unchanged; direct unsupported-shape delegation remains protected native dispatch, not a universal no-fallback opt-out.

#Human-format reads

When stdout is not a terminal, the shim renders pr view, pr checks, pr list, run view, run list, issue view, and issue list from the same relay-backed REST responses used by their --json forms. Their supported flags, filters, required numeric view identifiers, pagination bounds, and fallback behavior are identical to the existing --json interception paths. Terminal stdout still delegates to the real gh, preserving its interactive decoration. Unsupported presentation flags such as --web, --comments, and --template also delegate.

The native output follows real gh 2.x non-terminal formatting with these REST-driven differences:

  • PR authors show the login only, without the display name lookup; PR projects are empty.
  • PR reviewers come from the cached pull-review route, with each reviewer's latest state rendered in title case.
  • Issue projects, issue type, parent, sub-issue counts, blocked-by, and blocking fields are empty because the REST issue payload does not provide them.
  • Run-view headers use <head branch> <workflow name>, matching the verified GitHub run payload behind real gh; relative timestamps use coarse minute, hour, and day buckets.
  • Run views render jobs and their failed steps only; real gh's ANNOTATIONS and ARTIFACTS sections are omitted (those routes are not relay-backed), and the footer always points at the first job instead of varying by conclusion and job count. --log-failed and other log flags delegate to real gh.
  • Enabled auto-merge renders as enabled without the enabling user and merge method.

All relay-controlled single-line text has terminal control characters removed. PR and issue bodies preserve newlines and tabs while removing other control characters and invalid UTF-8.

Non-TTY pr checks uses the native checks-only table comparator, separately from JSON. Cancellation displays as fail while retaining its successful cancellation-only exit. Elapsed values display 0 when absent or nonpositive, and Go durations (including fractional seconds) otherwise. Watch retains Octopool's terse transition/final output.

Except for the common PR writes described below, after successfully fetching an explicit empty string rewrite policy (and finding no local rules), the command retains its existing real-gh delegation when any of these hold:

  • method is not GET, or mutating field flags are present (-f, -F, --field, --raw-field).
  • a query key looks secret-bearing, or a header is outside the safe set (accept, x-github-api-version, if-none-match, if-modified-since).
  • --jq was requested but jq is not installed.
  • a top-level subcommand or flag is not one of the supported read-only shapes.

Safe read-shaped requests are sent to Octopool first. Octopool owns the route and pool policy decision; on a cache miss it first uses token-free GitHub API/web/raw/Git endpoints for supported public shapes, then the configured app/PAT identity pool, writes eligible public responses to the shared cache, and returns the GitHub-shaped body. If the server says the read should run locally — unsupported route, public pooling disabled by policy, private/unverified repository, no usable identity, or identity pool depleted — the CLI runs the original command with the real gh and your local GitHub token.

Client-admission refusals (relay_overloaded) get three retries by default, with jittered exponential backoff of 0.5–1, 1–2, and 2–4 seconds. Other transient pool fallbacks and eligible relay service/connection failures keep one retry after one second by default. OCTOPOOL_RELAY_RETRIES overrides the maximum retry count for either case; 0 disables retries. Additional overload retries keep the 2–4-second range, but total retry sleep cannot exceed eight seconds. Once overloaded, retries also share the first relay read's timeout deadline, including subsequent slot waits and policy checks; a backoff that would exhaust either budget is skipped and the refusal follows the normal fallback path. Each shim relay GET or validated GraphQL read attempt has a 20-second timeout covering response headers and the body; policy fetches retain their separate timeout. A timed-out read does not retry and requests guarded local fallback with a relay_timeout diagnostic. An observed HTTP rejection remains a failure if its body times out. OCTOPOOL_NO_FALLBACK=1 keeps timeout and pool fallbacks as failures. Watch ownership remains as described above: run watches stop, and PR-check watches never hand off a client timeout after printing progress. These limits apply per relay read, not to the entire command, policy checks, or native gh execution.

To keep scripts spawning many gh processes from flooding the relay, CLI processes for the same user share eight advisory lock slots in a private directory under the OS temp directory (octopool-relay-slots-<uid> on Unix, with the user SID on Windows). Sandboxed and normal callers use this same primary pool. The server's default per-client limit is sixteen because it also counts detached cache refreshes and permits left by cancelled attempts; background refreshes leave room for all eight foreground slots. Equal client and server limits do not cover these different lifetimes. One slot covers an attempt's policy fetch, including its policy retries, and relay POST, avoiding a second queue wait between policy approval and dispatch. Standalone policy fetches use the same slots. A crashed process releases its lock automatically; slot files stay in place and must not be deleted while commands are running.

On macOS, the pool uses the canonical per-user temp directory under /var/folders/<x>/<y>/T/, so callers overriding TMPDIR still share the same slots. When TMPDIR already has that form, no subprocess is needed. Otherwise the CLI resolves /usr/bin/getconf DARWIN_USER_TEMP_DIR once per process with a one-second timeout; if resolution fails, it uses os.TempDir(). Linux and Windows retain os.TempDir(); temp-directory overrides (TMPDIR on Linux, TMP/TEMP on Windows) can split their pools.

Unix temp directories must belong to the current user and have no group/other permissions; Windows requires a protected current-user-only ACL. Insecure pre-existing directories and symlinks are rejected, not repaired. Callers that cannot create, open, or lock temp slots fall back to the OS cache directory's octopool/relay-slots folder (~/Library/Caches on macOS, $XDG_CACHE_HOME or ~/.cache on Linux, %LOCALAPPDATA% on Windows).

Slot acquisition polls with short jittered backoff for at most 30 seconds, then proceeds without a slot. Filesystem or lock errors fail open only if both directories are unusable; ordinary contention never switches directories. This is a best-effort cap, shared across pools and servers, and all processes should use the same OCTOPOOL_RELAY_CONCURRENCY setting and resolved temp directory. Cache and temp slots remain separate pools when temp is unusable. During a mixed-version upgrade, v0.9.4 normal callers still use cache slots and do not share the new temp cap; upgrade all CLI copies on the machine to share the primary pool. Initial waiting adds command latency before policy timeout/retry clocks and the relay read timeout start. Caller cancellation and earlier caller deadlines still apply. Relay retries acquire a new slot after backoff. Slots are released before relay retry backoff, native gh, jq, cache notices, or command output.

Pagination is fail-closed: if bounded relay pagination cannot prove a complete PR detail, check set, or filtered issue list, the CLI delegates to real gh instead of returning a partial result. Non-integer --limit/-L values are rejected explicitly.

With active string rewrite rules, modeled content, API, and lifecycle commands use strict structural validation and immutable sanitized snapshots. An unmodeled native command or flag no longer fails merely because Octopool does not recognize its shape: visible arguments, explicitly declared JSON/text stdin, and --input files are filtered on a bounded best-effort basis before normal real-gh delegation. Policy load/rewrite failures still block. Child exit codes and stdout/stderr are preserved. gh auth status normally delegates too. For gh auth status --active --hostname <host>, if GitHub's REST scope probe reports the active token as invalid while the same token still authenticates through GraphQL, the shim reports the account as authenticated and warns that reauthentication cannot restore REST quota. Broader multi-account status checks retain the real CLI's exit status and output, with a diagnostic when the active token still passes the GraphQL check.

gh auth login --with-token also stays delegated to the real CLI. If GitHub REST quota is exhausted while gh validates token scopes, the shim keeps the nonzero exit status but verifies the token through GraphQL and reports the core reset time. The token remains only in memory and is not stored by Octopool; retry the same login command after reset instead of reauthorizing.

#Common PR writes over REST

For non-TTY use on github.com, these exact command shapes use GitHub's REST API with your own native gh credentials, spending REST core quota instead of GraphQL points:

gh pr comment 123 --repo owner/repo --body-file comment.md
gh pr edit 123 --repo owner/repo --body-file description.md
gh pr close 123 --repo owner/repo --comment 'Closing comment'

Comment and body-only edit accept exactly one --body/-b or --body-file/-F (including - for stdin). Close accepts an optional --comment/-c, without --delete-branch. All require one positive numeric PR selector and accept --repo/-R; omitted repositories use the shim's local PR base-repository resolver, including GH_REPO and configured default remotes. Long flags accept separate or equals values; short value flags also accept attached values. Duplicate flags, other selectors/options, ambiguous repository context, Enterprise hosts, TTY output, and GH_FORCE_TTY retain guarded native handling. PR create and ready remain native.

The final string-rewrite guard runs first. REST consumes the prepared arguments and private body snapshot, so file and stdin content receive the same protection as native writes. Policy failures still block. Requests go directly to api.github.com, never through the relay or pooled identities. Authentication honors GH_TOKEN, then GITHUB_TOKEN, then native gh auth token --hostname github.com with the caller's environment and configuration. Credentials are never printed.

Comments POST to the issue-comments endpoint; body edits PATCH the pull-request endpoint. Comment and close first GET the pull request using the same caller credentials. That prevents posting to an ordinary issue and preserves close's already-closed and already-merged behavior. Close posts its optional comment before PATCHing state to closed. Comment and edit print the resulting URL to stdout; close prints native gh's non-TTY result to stderr, including ✓ Closed pull request owner/repo#123 (title).

Repository or credential lookup failures can delegate before any REST request or stdin consumption. REST errors return nonzero with a diagnostic. A repository rename may return one redirect to https://api.github.com/repositories/<id>/.... The CLI reads that repository's canonical owner/name and runs the same string-rewrite policy before it replays the write on /repos/<owner>/<name>/.... A forbidden canonical name produces no replayed write. Any other redirect is refused. Writes are not retried after an uncertain response, and the CLI does not fall back to native execution after an attempted write. A close can therefore leave its comment posted when the subsequent close fails, as native gh can. An interrupted response may mean GitHub accepted the write; inspect its state before retrying. Set OCTOPOOL_REST_WRITES=0 to restore guarded native execution for all these commands.

#octopool health [--pool <id>]

Fetches GET /v1/pools/<pool>/health using the stored token. Returns identity counts and policy version.

#octopool stats [--pool <id>] [--since 24h] [--client <name>] [--json]

Client filters and client groups use the same suffix policy for both current and historical audit labels. These are derived labels, not proof that credentials or physical machines were reconciled; stored audit names and caller/token links remain unchanged.

Fetches GET /v1/pools/<pool>/stats using the stored token. The default human output shows recorded relay request counts, service errors, expected local fallbacks, raw and successful-eligible cached-body reuse rates, coalesced duplicate misses, caller- and client-specific usage, all of the caller's active client usage, D1 cache entries, and top route kinds. When the server records backend attribution, it also ranks attributed relay outcomes by route and bounded source (github_web, anonymous github_api, or github_identity) and groups local delegation by fallback reason. No request bodies, query values, or credentials enter these aggregates. cache_served_responses counts hit and stale outcomes, including successful revalidations; uncached_outcomes counts miss and bypass outcomes, including failures and local fallbacks. Neither counts actual or avoided upstream requests. Cache serves may still need GitHub probes, and a miss may try several upstream requests. Unknown outcomes remain separate. The deprecated saved_github_requests and backend_requests JSON fields are historical aliases for these canonical fields, retained for shipped clients. The CLI also reads those aliases when connected to an older Worker. Stats cover relay-audited outcomes; policy GETs and failures before relay admission are not included, so these totals are not a census of all service requests. --since accepts 30m, 24h, or 7d style windows, capped at 30 days. --client filters client_usage and client_routes to that named client while retaining the authenticated caller's scope. Human output identifies both the calling client and the filter; JSON includes client_filter only when the filter is active.

octopool stats
# pool: maintainers
# client: steipete-mbp
# recorded relay requests: 54 (1 service errors, 2 local fallbacks)
# cache: 82.4% body reuse (40 hits, 2 stale, 9 misses, 3 bypass, 0 unknown)
# eligible: 49/54 requests, 85.7% body reuse
# coalesced: 4 duplicate misses
# outcomes: 42 cache-served, 12 uncached
# this client: 38 requests, 31 cache-served, 7 uncached
# top routes:
#   pr_view: 31 req, 86.1% eligible body reuse, 1 stale, 5 miss, 0 bypass, 0 errors, 1 fallback
# backend-attributed relay outcomes:
#   github_web / workflow_run_list: 12 req, 12 miss, 0 bypass, 0 revalidated
# fallback reasons:
#   identity_pool_depleted / contents: 1 req
# clients:
#   steipete-mbp: 38 req, 31 cache-served, 7 uncached, 1 fallback

Use --json for dashboards or scripts that want the raw aggregate:

octopool stats --since 7d --json

Filter the client-specific aggregates without changing the calling client identity:

octopool stats -client ci-runner

#octopool native-delegations [--since 24h] [--top 20] [--json]

Summarizes native gh dispatch attempts from this user's local journal, grouped by parent executable basename, category, and redacted command shape. The default window is 24 hours; --since accepts positive Go durations or whole days such as 7d. --top limits the sorted groups (default 20); the total always includes every matching record. No login or network access is required. --json prints since, total, and groups, with each group containing parent, category, shape, and count.

octopool native-delegations
octopool native-delegations --since 7d --top 30 --json

The journal is local-only and never uploaded. It defaults on, including for writes and internal native quota, viewer, preflight, and credential-lookup probes. Direct REST writes performed by Octopool itself are not native delegations. Each JSON line records the UTC RFC3339 timestamp, CLI version, parent PID and executable basename, grandparent basename when available, command shape, category, GraphQL involvement (true, false, or null when unknown), and whether OCTOPOOL_FRESH was present (not its value). macOS uses kernel process metadata without spawning ps; names may be truncated by the kernel. Linux uses /proc; unavailable process names and other platforms use empty strings. Categories include native-json-fields, graphql-delegated, include, write, unsupported-command, and local-fallback:<reason>. Known incomplete metadata reasons are grouped as metadata_incomplete; unrecognized error text becomes other.

Shapes retain recognized subcommands, flag names, and known --json field names only. REST API shapes include the method and a template such as /repos/:owner/:repo/pulls/:number; unrecognized segments become :segment. GraphQL shapes retain only a bounded named operation from an inline -f/-F query=..., or graphql otherwise. Bodies, query text and variables, tokens, URLs, repository names, selectors, numeric arguments, and file paths are never recorded. Query files and stdin are never read for journaling. Unknown commands, flags, and JSON fields are redacted, and argument/shape lengths are bounded. Flag ordering and repeated fields are normalized.

Records are stored in <user-cache-dir>/octopool/native-delegations.jsonl (0600 on Unix), using the same OS cache base as relay slots. If the cache cannot be written, the journal tries the existing private per-user relay-slot temp directory described above: <temp>/octopool-relay-slots-<uid>/native-delegations.jsonl on Unix, or the SID-based directory with a current-user-only ACL on Windows. Unix fallback directories must be owned by this user and have no group/other permissions. Insecure directories are rejected. The summary reads both locations and their .1 files, ignoring unavailable files and malformed or truncated records. Temp cleanup and rotation can shorten the requested window.

When the active journal exceeds 2 MiB, the next append renames it to .1, replacing the single previous generation. Appends use one O_APPEND write without fsync. A nonblocking local lock coordinates rotation; ordinary appends need no lock and rotation contention defers rotation without dropping the append. Concurrent writes can land in either retained generation. Unusable storage may drop an observation. Failures are silent and do not change command output or exit status. Counts describe native dispatch attempts, including failed starts; they are not GitHub request counts or measured GraphQL points. Set OCTOPOOL_NATIVE_JOURNAL=0 to disable recording; existing records remain readable.

#octopool request --path <p> [--method GET] [--query k=v] [--header k=v] [--route-hint k=v]

Debug/admin raw wrapper over POST /v1/github/request. Prints the full relay envelope. GET requests honor OCTOPOOL_FRESH=1; an explicit --header cache-control=<value> overrides that default, regardless of the header name's letter case. The effective headers pass through string protection before dispatch. This raw wrapper keeps its HTTP error output and does not retry or start native gh. --route-hint pr_head_sha=<sha> or --route-hint pr_state=closed can be used for state-aware PR subresource cache probes.

#octopool admin caller|identity|string-rewrites ...

Admin provisioning. Requires an admin token. See Admin & provisioning.

#Outbound string rewrite protection

While logged in, every protected gh command requires a fresh authoritative policy from GET /v1/pools/<pool>/string-rewrites. Each relay request and every final real-gh dispatch checks policy; a fallback cannot reuse approval for different arguments. There is no persistent policy cache, offline allowance, or fallback on authentication/policy errors.

Without a saved login or OCTOPOOL_TOKEN, the shim prints octopool: not logged in; using real gh without the relay or server string rewrite policy (run: octopool login) to stderr and runs real gh directly. There is no server policy in that state; local rules still apply at the native boundary. OCTOPOOL_NO_FALLBACK=1 keeps the missing login an error. A saved login that cannot be read, parsed, or bound to the target URL still fails closed with class=setup.

Policy HTTP requests reject redirects and bound the response to 65,536 bytes. Guarded policy GETs retain the existing 30-second timeout for the first attempt. After a transient failure, they may retry at most twice, only while less than six seconds have elapsed since the fetch started, including backoff. Each retry gets its own six-second timeout and may finish after that retry-start window closes. Transient HTTP 500/502/503/504 and Cloudflare 520–524 responses, HTTP 429, connection resets, interrupted responses, and network timeouts retry with jittered backoffs of about 300 ms and one second. A valid Retry-After (seconds or HTTP date) on these HTTP failures sets a minimum delay, capped at three seconds and subject to the retry-start window. Earlier caller deadlines and cancellation still take precedence. A slow successful first attempt remains valid; a failure received six seconds or more after the fetch started cannot retry. Other HTTP statuses, invalid policy documents, and local-file or merge failures never retry. These retries are independent of OCTOPOOL_RELAY_RETRIES; exhausted failures remain terminal, without native fallback or policy caching. Admin policy requests retain their 30-second deadline without automatic retries. Empty invocations, singleton help/version, known built-in topic help, and narrowly parsed GitHub authentication bootstrap commands remain available without a policy.

Topic help accepts exactly help <topic>, <topic> --help, or <topic> -h for these built-in paths:

  • Roots: api, pr, issue, release, auth, status.
  • PR: pr create|edit|comment|review|view|list|status|merge|ready.
  • Issue: issue create|edit|comment|view|list|status.
  • Release: release create|edit|view|list.
  • Auth: auth login|status, using long --help or help <topic> only.

For example, gh pr merge --help, gh pr ready -h, and gh help pr merge do not require policy access. auth login -h and auth status -h require a hostname value; auth login -h github.com and auth status -h github.com retain their narrow auth bootstrap route. Parent auth -h is help. Aliases, extension names, additional topics, operands, and options (including extended help spellings) follow ordinary policy and input protection. A value spelled --help, such as --body-file --help, still belongs to its input flag.

The same strict JSON file configures server rules and optional local rules:

{
  "schema_version": 1,
  "rules": [
    { "pattern": "\\binternal-model\\b", "replacement": "gpt-5.6-sol" },
    { "pattern": "\\binternal-family-[A-Za-z0-9_-]+\\b", "replacement": "" }
  ]
}

Local rules default to string-rewrites.json beside auth.json. Set OCTOPOOL_STRING_REWRITE_FILE to select another file. An absent default is optional; an explicitly configured missing or unreadable file fails closed. The local file is never uploaded. Server rules run first, then local rules; identical entries are deduplicated, conflicting replacements are rejected, and combined limits still apply.

Policy-load failures keep the existing message prefix and add bounded diagnostics to the normal stderr error line. After retries, diagnostics describe the last HTTP attempt and its subsequent validation checks; cancellation during backoff retains that last failure. There is no success diagnostic, stdout change, persistent log, or cached-policy fallback. A synthetic example:

error: string rewrite policy unavailable or invalid (class=http_status attempt_utc=2026-09-01T12:34:56.123Z elapsed_ms=42 http_status=403 cf_ray=0123456789abcdef-SJC)

The fixed class identifies the failing check, not its underlying cause:

ClassMeaning
setupCaller client setup failed, including saved-auth loading or saved-token URL binding. No policy request occurred.
requestPolicy URL, blank token, or HTTP request construction failed validation.
transportHTTP execution failed without a response, including DNS/network errors not classified below.
timeout / canceledHTTP execution reported a deadline/network timeout or context cancellation, respectively.
http_statusThe policy HTTP response was not exactly 200; redirects, including same-origin redirects, are refused.
response_readReading the 200 response body failed (including a timeout/cancellation while reading it).
response_sizeThe 200 response exceeded the 65,536-byte document limit.
server_validationThe 200 document failed JSON/schema/rule validation.
local_readLocal path resolution or bounded file reading failed, including an explicit missing, unreadable, nonregular, or oversized file. An absent optional default remains allowed.
local_validationLocal JSON/schema/rule validation failed.
mergeCombining individually valid server/local policies failed conflict or combined-limit checks.

attempt_utc is the client clock's UTC start time, before policy HTTP validation. elapsed_ms is monotonic elapsed time from that start to failure construction, truncated to whole milliseconds. It includes GET/body reading and any subsequent server/local/merge checks reached, but excludes caller client setup and final stderr formatting. For setup only, both fields instead cover client setup. They do not measure a whole gh invocation; each guarded boundary reloads policy, with a new timestamp for every HTTP attempt. Backoff and earlier attempts are excluded from elapsed_ms. The HTTP deadlines and retry-start window do not bound later local/merge work. An earlier caller deadline or cancellation still takes precedence.

http_status is omitted without an observed response. For server_validation, local_read, local_validation, or merge, http_status=200 belongs to the preceding policy GET, not to local validation. Its correlation metadata survives those later failures. Optional cf_ray accepts only one value with exactly 16 ASCII hex digits, optionally followed by - and three uppercase ASCII POP letters. Missing, malformed, overlong, or multiple values are dropped, never truncated. It is an unauthenticated correlation hint, not proof of origin. No raw error strings, URLs, paths, bodies, arbitrary headers, tokens, or rule content are included. Admin HTTP 409 remains a revision conflict; guarded caller GET 409 is a terminal policy failure. See policy-failure correlation before drawing conclusions from a later successful probe.

Rules use a portable RE2 subset: literal Unicode, captures/noncapturing groups, alternation, greedy/lazy repetition, bracket classes, dot, anchors, ASCII \b, \w, \d, \s and their uppercase complements, escaped punctuation, and \n, \r, \t, \f, \a, \v. Inline flags, lookaround, backreferences, named captures, Unicode properties, POSIX named classes, octal/hex/Unicode escapes, \C, and quoted-literal extensions are not accepted. Use literal Unicode characters instead of regex Unicode escapes. JSON escapes are decoded normally; invalid UTF-8, unpaired surrogates, duplicate keys, unknown fields, missing fields, and duplicate patterns are rejected.

Each rule replaces all matches once, in order. Replacements are literal ($1 stays $1); an empty replacement deletes text. A final scan checks every effective pattern again and blocks remaining or newly created matches. Empty-string patterns are rejected at load time; contextual zero-width matches abort before dispatch. Limits are 128 rules, 256 UTF-8 bytes per pattern, 1,024 per replacement, 65,536 per policy document, and 1,048,576 bytes of materialized/intermediate/final content. Match iteration is also bounded.

With active rules, the initial local publication vocabulary is deliberately conservative:

  • PR/issue create, edit, comment, and PR review: explicit title/body flags, body files, or stdin; numeric PR/issue selectors for existing items. PR creation pins --head to the current branch when the flag is omitted (detached HEAD and non-git directories are blocked); the head must already be pushed, since a pinned --head prevents gh from implicitly pushing or forking. --base is optional and defaults to the repository's default branch. --dry-run is accepted. Reviews require one explicit review action. PR/issue creation accepts one --label/-l and one --assignee/-a value (use comma-separated lists), while PR/issue edits accept metadata-only add/remove label and assignee flags. PR edits also accept --base / -B, alone or with explicit title/body updates. Base branches must pass Git branch-name validation and structural rewrite checks; branch names are never rewritten. Assignees may include native @me or @copilot. PR/issue create/edit/comment also accept repeated --attach image/video files as described below.
  • Release create/edit: explicit title/notes or notes files and one tag. Creation requires --verify-tag, so gh cannot create a missing tag. Generated notes, notes from tags, and other implicit content sources are blocked. Asset-bearing creation additionally requires explicit --draft=true (or --draft) and unchanged metadata, as described below. Release editing still accepts exactly one tag and no assets.
  • Raw REST API equivalents: allowlisted issue/PR/review/comment/release endpoints with -f/--raw-field, -F/--field, or --input JSON. Literal and typed fields retain their distinction; only typed @file/@- values read files/stdin. Nested review comments use --input JSON. Exact issue-assignee POSTs accept repeated raw assignees[] values; exact pull-request merge PUTs accept merge_method values squash, merge, or rebase, leaving the field omitted for GitHub's default, with optional rewritten commit_message and commit_title strings. A supplied sha must be a full 40-hex commit; when omitted, a guarded local GET resolves the current PR head and pins it into the immutable merge snapshot. Async PUT /repos/OWNER/REPO/pulls/NUMBER/merge-async has the same snapshot and head pinning, plus merge_action (default, direct_merge, or merge_queue) and Boolean bypass_rules. These authority fields are preserved or rejected, never rewritten; only commit title/message text is rewritten. Omitted method/action/bypass fields retain GitHub's defaults. Status GET .../merge-async/UUID and GET .../merge use the local writer's credentials, with a pinned endpoint and empty stdin, without the read relay. Local --jq/-q and --template/-t output formatting stays supported without rewriting formatter syntax. Unsupported request syntax, payloads, query parameters, pagination, response caching (--cache), and raw-body headers (Content-Type) fail before dispatch. Use the modeled JSON input directly instead of a raw-body header. Callers that require this contract across CLI upgrades should put -H 'X-Octopool-Require: merge-async-v1' immediately after gh api, before all other flags. Protected older CLIs reject this unknown header before rewriting or child I/O; this CLI consumes it locally for modeled merge requests. With no active rewrite rules, native gh receives the header unchanged and performs no policy rewriting. Upgrade the CLI if this marker is refused; do not remove it to work around protection. No Worker deployment, login change, or policy change is required. Pull-request PATCHes accept a string base field with the same branch checks as the CLI; base-only updates do not republish an implicit title or body. Numeric branch names must be supplied as strings, such as -f base=123 or JSON {"base":"123"}. Other bracket accumulation, duplicate keys, mixed input/field sources, unknown properties, and custom authentication headers are rejected. Raw release creation first verifies the existing remote tag with a local authenticated GET. Payload validation errors identify the schema, field, and expected shape without echoing submitted values or rewrite rules; policy rejections retain a generic protection error.

Long flags accept separate or equal values; supported short value flags also accept attached values. Repeated flags/aliases and boolean clusters are rejected. Metadata flags not listed by the guard are unsupported; use an allowlisted raw REST shape where applicable. Creation also rejects sanitized empty titles/bodies/notes to avoid implicit defaults. Repository context is normalized and pinned into an explicit owner/repo after validation; explicit github.com/owner/repo, https://github.com/owner/repo, and GitHub SSH forms are accepted, while other hosts are blocked. Protected native children force GH_HOST=github.com and remove inherited GH_REPO, so normalization cannot redirect an explicit GitHub.com repository to an ambient Enterprise host. Input files are read into bounded snapshots, never modified, and never reopened by the child. Sanitized snapshots use a private 0700 directory and 0600 files, removed after execution, including nonzero exits. These modeled commands do not retain live stdin.

With active rules, gh release create TAG --repo OWNER/REPO --draft --verify-tag --title TITLE --notes-file NOTES FILE... accepts up to 16 explicit local asset paths on macOS, Linux, and Windows. The explicit --repo github.com/OWNER/REPO spelling is also supported, including --repo=..., -R ..., and -R...; the same metadata and asset protections apply. Each asset must be a nonempty regular file, at most 1 GiB; the aggregate asset limit is 4 GiB. Public basenames are limited to 255 bytes and use only ASCII letters, digits, internal periods, underscores, and hyphens. A basename cannot start with a period or hyphen, or end with a period; a leading underscore is allowed. Empty and Windows reserved names remain rejected. GitHub documents filename rewriting during asset uploads without specifying a complete normalization algorithm. This is Octopool's conservative supported subset; other public names are rejected rather than silently sanitized. The existing 1 MiB text budget remains unchanged. Title, notes, original/resolved source paths, and exact public basenames undergo policy, material, residual, and UTF-8 checks; if filtering would change any of them, the entire command is rejected. Canonical notes are preserved byte for byte. Assetless release creation retains its existing filtering.

Asset operands cannot be stdin, URLs, unexpanded globs, #label forms, traversal paths, symlinks, Windows UNC/device paths, or unsafe portable filenames (including control characters, Windows reserved names, and trailing dots/spaces). Public-name restrictions apply to the original basename before filesystem access. Source components have separate validation: .claude, spaces, Unicode, and leading hyphens are allowed in parent directories and private staging paths, subject to the existing path and policy checks. Duplicate basenames, case-fold collisions, hard links to the same file, and resource-limit violations are rejected. Parent directory aliases such as macOS /tmp are resolved and checked; Windows reparse points, including ancestor reparse points, are rejected.

Generated release asset paths undergo the same literal-operand validation as source operands, in addition to policy checks. A temp-root path containing # or glob syntax therefore blocks asset-bearing creation and cleans up staging before native gh starts: gh release create interprets asset operands as filenames/patterns with optional #label syntax. Ordinary metadata snapshots use filesystem paths rather than release operand syntax; legal # or bracket characters in a private temp directory remain supported there. Windows shared staging still enforces local filesystem, device, traversal, reparse, handle-pinning and private ACL checks. Hidden, space-bearing and Unicode temp directories remain supported for release assets when their paths contain no release operand syntax.

Every asset is copied in 64 KiB chunks into exclusively created private snapshot files before native gh starts, preserving names, order, and opaque bytes. Descriptor identity and before/after file metadata reject observed replacement and size changes. Bounded SHA-256 passes compare the source before and after copying with the bytes written to the snapshot, rejecting in-place mutation even when file timestamps are unchanged. This reads each source three times with constant memory; the same size limits and cancellation checks apply to every pass. Unix opens do not follow symlink operands and cannot block on a substituted FIFO. Windows requires persistent filesystem ACLs, creates staging with a protected current-user-only inheritable ACL, pins directory handles against deletion, and excludes source write/delete sharing while capturing. Cancellation is checked between chunks and before execution. Snapshots are removed on preparation/start/child failures and handled cancellation; uncatchable termination and power loss can leave temporary files behind.

Assets, including checksums and provenance, are opaque: Octopool does not rewrite, unpack, rebuild, sign, or certify their contents as secret-free. The caller owns artifact review, provenance, and a reviewed, frozen source directory. Staging isolates subsequent source changes; these checks cannot prove correspondence with earlier verification or prevent arbitrary hostile local writers from coordinating changes during capture. There is no digest handoff. Local preparation is all-or-nothing, but remote draft creation and upload are not a transaction. A failed child can leave a partial remote draft. Octopool does not delete drafts, replace assets, clobber, retry uploads, or publish after failure. This capability does not add modeled standalone release upload support or new release-edit forms.

Protected PR/issue create/edit/comment commands accept up to 16 repeated --attach FILE or --attach=FILE values. Raster image extensions are GIF, JPEG/JPG, PNG, and WebP; video extensions are MOV, MP4, and WebM. Images are limited to 10 MiB each, videos to 100 MiB each, and all attachments together to 100 MiB. The source path is structurally checked, optional #alt text is rewritten, default image alt text remains based on the original filename, and native gh receives suffix-preserving private byte-for-byte snapshots. Recognized inline Markdown links/images that point at an attached local file are rewritten to the matching snapshot, so native gh can preserve labels, alt text, titles, and normal upload-URL replacement. Up to 64 matched inline references are rewritten with bounded parser verification. Code spans remain literal; matching reference-style definitions are blocked rather than published with broken paths. Empty files, directories, other extensions, and video alt text are blocked. A new PR/issue comment may consist only of attachments, including with --edit-last=false. PR/issue edits with attachments and PR/issue comment --edit-last (or --edit-last=true) attachment updates require an explicit complete --body or --body-file, including when --create-if-none is set, because native gh would otherwise fetch and republish existing text after the guard runs. Title or metadata flags do not satisfy this requirement for PR/issue edits. PR/issue edits allow an explicitly supplied empty body after inspection. Other modeled body requirements stay unchanged; issue creation still requires an explicit title and body. Attachment bytes are not text- or vision-inspected, so callers remain responsible for reviewing the media itself before publication.

Commands and flags outside the modeled vocabulary use a bounded best-effort pass-through instead of being denied solely for being new or unfamiliar. This includes commands with only one argument, such as gh browse; adding a flag is not required to reach guarded native execution. Octopool rewrites every visible argument, snapshots and filters --input files or --input=-, snapshots typed -F/--field key=@source text without changing its formatting, and filters stdin when the command explicitly declares it (currently workflow run --json). Nonempty declared JSON must pass strict UTF-8, surrogate, decoded duplicate-key, single-value, and depth validation; malformed JSON never falls back to text. Decoded string keys and values are rewritten and the final JSON is bounded again. This keeps workflow dispatches, job-log reads, unmodeled uploads, and newly introduced native flags working within the declared-input boundary. Native children force GH_HOST=github.com and remove inherited GH_REPO; best-effort --repo/-R values are structurally checked before rewriting and normalized to github.com/owner/repo. Native gh search repository filters retain owner/repo, because the CLI converts that value into a repo: search qualifier and rejects a host-qualified form. Repo-capable commands without an explicit selector pin the current GitHub.com remote. Alternate API/repository hosts, explicit credential headers, unresolved API placeholders, invalid UTF-8, policy material, residual matches, and bounded-read failures remain blocked.

For native gh api --input, a missing or empty effective Content-Type defaults to JSON; explicit JSON and +json media types also require strict JSON. A valid explicit non-JSON Content-Type keeps bounded UTF-8 text filtering without JSON reformatting, including JSON-looking text. Content-Type is a native capability, not an additional relay header. Repeated headers follow native Header.Add order: an empty first Content-Type selects the native JSON default; otherwise declarations must be equivalent, and conflicting or malformed media types block. Exactly zero-byte best-effort input retains native no-input compatibility; whitespace-only declared JSON blocks. Modeled API empty-body rules remain unchanged.

Workflow JSON flags accept native Boolean spellings (1/t/T/true/TRUE/True and 0/f/F/false/FALSE/False), validate every occurrence, and use the final assignment. Bare --json means true; --json false includes a positional false. Absent or final-false JSON flags do not cause preparation to read idle stdin. True workflow help also leaves JSON stdin unread; native short -h requests help immediately, while long --help uses its final Boolean assignment. Known API -i bundles preserve each include option and the following value-taking flag. Parser metadata never adds synthetic flag names or default values to the text being rewritten. Known option values retain their ownership, including flag-looking values and interleaved repository options. A real -- ends declarations: subsequent arguments are visible text, not file/stdin sources or headers. Generated repository/hostname pins precede that delimiter.

Original sources are captured before argument rewriting, and newly introduced sources are captured afterward. The stricter original/final JSON declaration applies to captured bytes; rewriting cannot downgrade inspection or reopen a substituted original path. Typed field sources remain literal text, and raw fields remain literal arguments. Only other commands' undeclared input retains the previous opportunistic JSON-or-text handling. These checks do not infer formats from filenames, endpoints, or bytes, decode nested obfuscation, or protect against downstream reinterpretation of deliberately mislabeled text. Binary API input has no exemption from UTF-8 validation. The existing 1 MiB content and aggregate budgets apply.

For gh repo clone, positional repository checks apply only to the source, accounting for native clone options and their values. A -- ends native option parsing; if the source has not appeared yet, the next argument is still the source. Unknown options before the source fail closed. Source protocol is preserved, with structural and GitHub.com host validation before and after rewriting. Destination paths and forwarded Git flags and values still receive visible-argument filtering without being treated as additional repository inputs.

Best effort is intentionally not the same guarantee as a modeled snapshot. Live terminal input is passed through so native prompts still work, and deferred content sources other than --input and typed field @source files—for example an editor, generated notes, extension-owned files, or content native gh derives after the guard—cannot be inspected. Direct real gh, browsers, Git pushes, and deliberate encoding/obfuscation remain outside the boundary. Use a modeled command when publication content needs the strict guarantee. Reads are byte-bounded, not time-bounded: an arbitrary producer can stall preparation before EOF or the limit. Existing child cancellation and snapshot cleanup do not make a blocked arbitrary reader interruptible.

Modeled content paths also reject recognizable active rule material before rewriting and in the final text: complete JSON objects with exactly string pattern and replacement fields, whose decoded pattern equals an effective server or local pattern. The replacement need not be identical. This covers copied policy files, compact or pretty JSON, rule arrays, JSON Unicode escapes, and ordinary Markdown fenced snippets, including inline text, files, stdin, and every decoded REST payload string. Fenced regions are scanned with independent parser state so unrelated quoted prose cannot hide a complete rule object. Detection is content-based, not filename-based, and stays within the existing 1 MiB limit. Ordinary prose containing a forbidden term still undergoes normal rewriting; merely mentioning regex source outside a recognizable JSON rule is not classified as policy material.

Reads check decoded path segments, query keys/values, and safe forwarded headers, including bounded percent-decoding layers. Matches in structural values are blocked, never rewritten. Active policy also rejects unresolved native-gh placeholders anywhere in the original REST endpoint, including query keys/values and embedded :owner, :repo, or :branch forms, before initial dispatch and every final delegation. Supply literal structural IDs. The relay rejects literal TAB/LF/CR in paths before URL parsing and checks canonical final outbound URLs and noncredential headers, including derived probes and followed redirects. An encoded %09 remains encoded on the wire; it is not treated as a stripped literal TAB. This includes direct octopool request with local rules and approved local fallbacks. Allowlisted top-level reads may fall back to native gh after Octopool pins and checks the repository, numeric/ref selector, JSON field projection, filters, and composed request. This covers readiness/CI projections, issue comment projections, and PR head filters whose GraphQL shapes are outside the relay's fixed allowlist. Numeric pr ready (or a checked current/explicit nonnumeric Git branch) and metadata-only PR/issue edits with add/remove label or assignee flags are also allowed without free-form text. Exact-head pr merge --squash --match-head-commit with a numeric selector is converted, when the final policy has active rules, to the immediate pull-request merge REST endpoint with the checked SHA and squash method; it never enables auto-merge, so a branch that requires a merge queue fails closed. An explicit --body-file/-F (including - for stdin) supplies an optional commit message through the same bounded text rewriting and private JSON snapshot as REST content. An optional --subject/-t supplies commit_title through that same protection, with separate, equals, and short attached values supported. Subjects are literal text: a leading @ never reads a file or stdin. The child receives only the checked JSON snapshot, not the original subject, body path, or live stdin. Omitting the subject leaves commit_title absent.

Explicit pr merge --auto --squash --match-head-commit FULL_SHA also accepts a numeric selector, but requires both a nonempty --subject/-t and an explicit --body-file/-F. The subject and body pass through the same bounded text rewriting; a subject that becomes empty or whitespace is rejected so native gh cannot generate an uninspected headline. An explicit empty body is allowed. Native gh receives the sanitized subject value and private body snapshot, an explicit repository, the full head SHA, and empty stdin. Original body files are never modified or reopened by the child. --auto=false retains the immediate REST path. Admin privileges, other merge methods, disabling auto-merge, inline-body flags, URL selectors, and unpinned merges remain blocked on this strict path.

The auto path preserves native gh semantics: it can merge immediately when requirements are already satisfied, otherwise it enables auto-merge. The expected SHA constrains the submission; it is not a permanent head freeze. Later pushes, requirements, and merge-queue behavior remain GitHub-owned, and queue policy may determine the final merge method and metadata. A caller requiring an exact eventual commit must continue observing and reconciling the PR. The immediate REST path never silently opts into that lifecycle.

Other editor/web/template/fill modes, unmodeled uploads, aliases/extensions, raw GraphQL, and newly introduced native commands/flags use best-effort filtering and retain native behavior. A denial reports only the generic unsafe-input boundary and never echoes the rejected text.

An empty final policy retains native gh pr merge; the final policy decision can differ from an earlier check. Neither route changes the local writer's authentication.

Admin imports always fetch the current revision before the conditional update:

octopool admin string-rewrites status
octopool admin string-rewrites set --file rules.json
cat rules.json | octopool admin string-rewrites set --file - --if-revision 7

--if-revision additionally compares the operator's expected revision. A conflict never overwrites a newer policy. Status and successful imports print only revision and rule count, not patterns, replacements, matched content, or local paths.

Strict protection covers modeled traffic through this updated shim and relay, using the policy snapshot checked before dispatch; unmodeled native commands receive the best-effort filtering described above. Direct real gh, interactive/deferred native content, browsers, Git pushes, older clients' local writes, existing published content, and arbitrary deliberate obfuscation are outside this boundary. Policy-material detection does not interpret arbitrary Markdown, Unicode obfuscation, malformed rule JSON, or arbitrary encodings, and is not general data-loss prevention or a network-wide guarantee. GitHub writes continue to use the user's local credentials; the Worker remains GET-only for REST, with the separately validated repository GraphQL POST read.

#Cache freshness

Shared cache hits are the point of the relay, but a cached answer describes the state at fill time, not now. An open PR's server TTL scales with 10% of its age since updated_at, between two and five minutes. Closed-unmerged PRs range from two minutes to one hour; merged PRs keep one hour. Plain completed runs range from one to ten minutes, while active runs stay at one minute. Missing or unusable timestamps keep the minimums. See cache TTLs for the full policy and unchanged outage stale windows. Cached reads can therefore lag a push, re-run, merge, or reopening unless the caller requests a tighter age bound.

Three things keep that honest:

  • Gate fields read live. gh pr view --json reads that include headRefOid, baseRefOid, state, merged, mergedAt, mergeable, mergeStateStatus, autoMergeRequest, maintainerCanModify, mergedBy, closedAt, or statusCheckRollup send cache-control: max-age=0 automatically. These are the values callers branch on, so they require an upstream fetch or successful conditional revalidation. Descriptive fields (title, body, labels, author) stay cached. Rollup check/status pages and head-filtered runs read live; workflow names use the shared one-hour catalogue cache with one live retry if a verified run's workflow name is missing.
  • Cached decision reads announce themselves. When a PR, issue, run, or checks route is served from the shared cache, the CLI prints one line to stderr naming the route, whether it was a hit or a stale serve, and when it refreshes. stdout stays untouched, so --json and --jq consumers are unaffected. A stale response warns that it is unsafe for live decisions even under OCTOPOOL_FRESH=1; older relays may ignore the requested bound. A hit remains visible under FRESH: the response does not distinguish live revalidation from an ordinary cache hit, and explicit cache-control headers can still allow cached reads. Silence cache notices with OCTOPOOL_QUIET_CACHE=1.
  • Anything can request a live read. OCTOPOOL_FRESH=1 applies max-age=0 at shared relay request construction, including top-level run view and every jobs hydration request. An explicit cache-control header retains precedence, including headers passed through gh api -H. Every explicit age bound also applies during outages: max-age=0 cannot return an unvalidated cached success. If upstream cannot satisfy the bound, the existing typed failure/local-fallback flow applies. Reads without an age bound retain the relay's route-limited stale outage fallback.

Note that git push never passes through Octopool, so the relay cannot invalidate a PR entry when a branch moves. That is why the gate fields read live rather than relying on invalidation.

#Token and URL safety

  • A saved caller token is only sent to the saved Octopool URL. Overriding --url (or OCTOPOOL_URL) to a different host requires an explicit OCTOPOOL_TOKEN, or a fresh octopool login for that URL. This prevents leaking the token to an attacker-supplied endpoint.
  • request, health, and stats bind the saved URL and caller token to the same loaded auth snapshot, so another login cannot replace the token midway through request setup. An explicit nonblank token environment variable (--token-env, default OCTOPOOL_TOKEN) still takes precedence over the saved token.
  • Login, caller, and admin JSON requests follow redirects only to the same scheme, hostname, and effective port as the original request (omitted ports mean 80 for HTTP and 443 for HTTPS). Cross-origin redirects and HTTPS downgrades fail before the target receives a request. Neither --trust-discovery-redirect nor OCTOPOOL_ALLOW_INSECURE_LOGIN=1 bypasses this restriction. Configure the intended API base directly when moving a server to another origin. Credential-free discovery keeps its existing redirect behavior; string-rewrite policy requests still reject all redirects.
  • Once a request reaches Octopool, relay policy denials fail closed; they are not silently retried against the real gh unless Octopool returns the explicit fallback_local signal for a safe read.

#Environment variables

These are dev/CI escape hatches, not the everyday UX:

  • OCTOPOOL_URL — base URL override.
  • OCTOPOOL_TOKEN — caller token override (required to use a non-saved URL).
  • OCTOPOOL_POOL — pool id (default maintainers).
  • OCTOPOOL_GH_PATH — path to the real gh binary.
  • OCTOPOOL_NATIVE_JOURNAL=0 — disable the default-on, local-only native delegation journal. Records are never uploaded; inspect existing counts with native-delegations.
  • OCTOPOOL_REST_WRITES=0 — disable common PR REST writes and retain guarded native gh.
  • OCTOPOOL_GRAPHQL_RELAY=0 — keep arbitrary repository GraphQL queries native; the existing exact landing projections retain their pooled routing.
  • OCTOPOOL_DIAGNOSTICS=1 — opt-in final PR-merge dispatch and bounded response-metadata diagnostics on stderr; only the exact value 1 enables them. See quota provenance above.
  • OCTOPOOL_STRING_REWRITE_FILE — optional local policy JSON; an explicit unreadable or missing file fails closed. Defaults to string-rewrites.json beside auth.json.
  • OCTOPOOL_FRESH=1 — default every relayed read to cache-control: max-age=0, including direct octopool request GET reads, run metadata, and jobs; explicit cache-control headers take precedence. Identity lookups for PR author metadata stay cache-eligible with a one-hour age bound. Use it right after a git push or a merge, when a cached answer could describe the previous state. It can cost API quota; leave it off for ordinary reads. Outage fallback cannot exceed the requested age bound.
  • OCTOPOOL_QUIET_CACHE=1 — suppress the one-line stderr note printed when a decision-shaped route (PR/issue/run/checks) is served from the shared cache.
  • OCTOPOOL_NO_FALLBACK=1 — fail instead of running real gh after Octopool returns fallback_local or a shim relay read times out, including during an active watch, useful for probing known-supported relay reads. Deliberately unsupported command shapes still delegate directly, so success alone is not universal proof of relay use. Check the actual execution context and routing evidence; a separate sandbox wrapper may not use Octopool.
  • OCTOPOOL_RELAY_RETRIES — how many times transient pool-exhaustion fallbacks (identities_cooling_down, identity_pool_depleted, github_identity_depleted, github_rate_limited, relay_overloaded), relay 5xx internal_error responses, and malformed 502/503/504 or Cloudflare 520–524 gateway responses are retried against the relay. Defaults to 3 for relay_overloaded, with jittered 0.5–1s, 1–2s, then 2–4s delays, and 1 for other failures, with 1s before every retry. A nonnegative integer overrides both counts; invalid/negative values use the reason-specific default, and 0 disables retries. Overload retries remain bounded by eight seconds of total retry sleep and the first relay read's timeout deadline even with a larger override. Interrupted response bodies and transient connection failures on safe relay reads use the same budget; timeouts never retry. Every retry obtains current protection policy; policy failures, authentication denials, response-size violations and caller cancellation never become retries or native handoffs. Exhausted transient fallbacks may delegate to real gh except for supported gh run watch, which fails explicitly. Exhausted service errors remain failures instead of spending local GitHub quota.
  • OCTOPOOL_RELAY_CONCURRENCY — concurrent relay attempts and standalone policy fetches across CLI processes for the same user on this machine (default 8; 0 disables). Nonnegative integers are accepted; invalid or negative values use the default. Each acquisition waits up to 30 seconds before failing open without changing command errors. All callers prefer private per-user temp slots; unusable temp slots try cache slots before failing open.
  • OCTOPOOL_RELAY_TIMEOUT_SECONDS — per-attempt timeout for shim relay GET and validated GraphQL reads, including response-body reads. Default 20; nonnegative integer values below 5 are clamped to 5 seconds. Invalid, negative, or unrepresentable durations use the default. After relay_overloaded, the first read's deadline bounds remaining retries, including their slot waits and policy checks. A relay HTTP timeout requests guarded native fallback with reason relay_timeout, without retrying or claiming an explicit server fallback_local response. Login, writes, policy fetches, and direct octopool request retain their existing timeouts. Policy failures remain terminal, including when an overload retry's deadline interrupts policy acquisition.
  • OCTOPOOL_ADMIN_TOKEN — admin token for octopool admin.
  • OCTOPOOL_ALLOW_INSECURE_LOGIN=1 — permit non-HTTPS login for local dev.

#Server discovery

Self-hosted Octopool servers expose:

GET /.well-known/octopool

Response shape:

{
  "service": "octopool",
  "version": 1,
  "api_base": "https://octopool.your-org.dev",
  "app_base": "https://octopool.your-org.dev",
  "default_pool": "maintainers",
  "allowed_org": "your-org",
  "auth": {
    "cli_github_token": true,
    "web_login": true
  },
  "min_cli_version": "0.2.2"
}

octopool.dev advertises itself as the API base and https://octopool.openclaw.ai as the browser app base. A one-domain self-host can use the same origin for both.