You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Describe the feature or problem you’d like to solve
gh frequently exposes implementation details from the API client instead of explaining the user’s failed goal:
GraphQL: Could not resolve to a PullRequest with the number of 45.
The problem is larger than the GraphQL: prefix. Today, errors may:
Lead with GraphQL, HTTP, mutation, or field-path details irrelevant to most users.
Flatten several validation failures into one sentence.
Suggest remediation that is wrong for the active credential type.
Report complete failure after a resource was successfully created.
Treat warnings as command failures.
Encourage unsafe retries when a remote side effect may already have happened.
Force scripts and agents to parse unstable human prose.
This behavior is inconsistent. gh already has better patterns for some DNS, authentication, SAML, and OAuth-scope failures. The goal should be to generalize those patterns rather than fix errors command by command.
#12596 and its revert in #12915 illustrate why changing strings alone is insufficient. The improved message depended on parsing server prose, broke an internal string comparison, and recommended gh auth refresh for credentials that could not necessarily be refreshed.
Proposed solution
Introduce a shared diagnostic model with:
Typed transport facts from the API layer.
Semantic translation at the command or domain layer.
Central human and JSON renderers.
Explicit command outcomes and exit statuses.
A sanitized fallback for errors not yet translated.
Design principles
An excellent gh diagnostic should:
Describe the user’s failed goal, not the API transport.
State what succeeded, what failed, and what remains uncertain.
Offer a safe, credential-aware next step when one is known.
Preserve missing-versus-inaccessible ambiguity where required.
Keep stdout usable as the successful result stream.
Provide stable structured data without freezing human prose.
Remain useful without color or a TTY.
Reveal technical details through GH_DEBUG, not by default.
Never suppress independent failures unless a typed relationship proves they are cascading.
Human diagnostic grammar
Centrally rendered diagnostics should use consistent roles:
error: <what gh could not accomplish>
<flag or argument>: <specific cause>
note: <relevant context>
help: <safe recovery step>
The headline severity is one of error, warning, or notice. note: and help: are optional.
Human messages should be neutral and non-blaming. TTY detection may change color and layout, but not meaning.
Request IDs should appear in structured output whenever available. Human output should show them only when contacting support or diagnosing a service failure is relevant.
gh api is an intentional exception to transport-neutral output. Its users selected an API-level command, so HTTP and GraphQL details can remain relevant. Extensions should continue to own their stderr.
Example diagnostics
These are illustrative. A translator should only assert facts the client can establish.
1. Missing or inaccessible resource
Before:
$ gh pr checkout 45GraphQL: Could not resolve to a PullRequest with the number of 45.
After:
$ gh pr checkout 45error: could not find pull request 45 in cli/clinote: the pull request may not exist or your account may not have accesshelp: check the repository or run `gh auth status`
2. Multiple invalid inputs
Before:
pull request create failed: GraphQL: Head sha can't be blank, Base sha can't
be blank, No commits between main and feature, Head ref must be a branch
After:
$ gh pr create --base upstream/main --head origin/featureerror: could not create the pull request because 2 branch references are invalid --base: expected a branch name instead of a remote-qualified reference --head: branch "feature" is not available in the selected head repositoryhelp: use `--base main` and push the head branch before retrying
Independent problems remain visible. A command-level summary avoids arbitrarily declaring one of them the primary cause.
3. Credential-aware permission remediation
For a refreshable token:
error: could not add the issue to project "Roadmap"
note: the active token is missing the "project" scope
help: run `gh auth refresh -h github.com -s project`
For GH_TOKEN:
error: could not add the issue to project "Roadmap"
note: the token supplied through GH_TOKEN lacks project access
help: replace GH_TOKEN with a token that has permission to update the project
A runnable command should only be emitted when it is known to apply. GitHub.com, GHES, Actions tokens, GitHub App tokens, environment tokens, and credentials managed by gh can require different recovery steps.
4. Partial success
If the issue was created but its type could not be set:
stdout
https://github.com/OWNER/REPO/issues/123
stderr
error: issue #123 was created, but its type could not be set
note: your account does not have permission to set issue types in OWNER/REPO
help: update the issue after obtaining the required repository permission
The process exits 3. The successful resource remains on stdout, and structured stderr repeats a minimal reference to it.
This avoids recovery instructions that would create a duplicate issue.
5. Internal service failure
Before:
GraphQL: Something went wrong while executing your query. Please include
`ABC1:1234` when reporting this issue.
After:
error: GitHub could not complete the request
note: request reference ABC1:1234
help: try again; if the problem continues, include the request reference when contacting support
Transport type, API path, and redacted raw facts remain available under GH_DEBUG.
6. Indeterminate remote outcome
A network timeout does not always mean that a mutation failed. The server may have completed it before the connection was lost.
error: gh lost the connection before it could confirm whether release v1.0.0 was created
help: run `gh release view v1.0.0` before retrying
The process exits 5. This is distinct from both failure and partial success.
Non-fatal diagnostics
Warnings and notices do not imply failure:
warning: Projects (classic) information is unavailable
note: the remaining issue details were retrieved successfully
The result remains on stdout and the process exits 0.
Structured diagnostics
Add a global format independent of command-specific --json output:
The envelope is written to stderr. Normal command results remain on stdout.
One envelope is emitted per invocation rather than compiler-style diagnostic streaming:
{
"schemaVersion": 1,
"outcome": "partial_success",
"command": ["issue", "create"],
"diagnostics": [
{
"severity": "error",
"category": "authorization",
"message": "Issue #123 was created, but its type could not be set.",
"context": {
"action": "set",
"resource": "issue_type"
},
"causes": [
{
"message": "The active account cannot set issue types in OWNER/REPO."
}
],
"remediation": [
{
"kind": "instruction",
"description": "Update the issue after obtaining the required repository permission.",
"applicability": "manual"
}
],
"support": {
"requestId": "ABC1:1234"
}
}
],
"result": {
"resources": [
{
"type": "issue",
"identifier": "123",
"url": "https://github.com/OWNER/REPO/issues/123"
}
]
},
"rendered": "error: issue #123 was created, but its type could not be set\n..."
}
Stability
Stable in schema version 1:
Field meanings and enum values.
Outcome and category semantics.
Typed remediation and resource-reference shapes.
Explicitly unstable:
message
description
rendered
These remain human prose and must not be parsed by automation.
Outcomes and exit statuses
Outcome
Exit status
Success, including warnings or notices
0
Failure
1
Cancelled
2, unchanged
Partial success
3, new
Authentication failure
4, unchanged
Indeterminate remote outcome
5, new
Pending
8, unchanged
The diagnostic outcome for an authentication error remains failure; exit 4 preserves the existing shell-level distinction.
Recovery categories
Version 1 uses a deliberately broad closed set:
usage
authentication
authorization
validation
not_found
conflict
rate_limit
network
service
unknown
These describe recovery classes rather than GraphQL or HTTP implementation details. A granular stable error-code registry can be considered separately.
Remediation actions
Remediation entries have a typed kind such as:
command
documentation
instruction
They also declare applicability:
exact: safe and directly runnable.
conditional: applies if a stated condition is true.
manual: requires user judgment or external changes.
Commands should be represented as argument arrays rather than shell strings.
Proposed Go architecture
Suggested package layout:
api/
client.go structured transport facts only
internal/diagnostics/
diagnostic.go model and enums
report.go invocation outcome and resources
collector.go warnings, notices, and results
render_human.go
render_json.go
internal/diagnostics/translate/
api.go
auth.go
network.go
fallback.go
internal/ghcmd/
cmd.go assemble, render, and choose exit status
A diagnostic error returned by a command should preserve its underlying cause through Unwrap, while carrying semantic context and an optional partial result.
cmdutil.Factory should expose an injected diagnostics.Sink. Migrated commands use it instead of immediately printing warnings. This allows internal/ghcmd to combine non-fatal diagnostics with a returned error and render exactly one structured envelope.
Error flow
api/ preserves structured HTTP, GraphQL, header, and request metadata without choosing human prose.
A command adds user-level action, resource, flag, and result context when it has it.
Generic translators handle authentication, authorization, network, service, and typed API failures.
Root merges collected warnings with the returned diagnostic error.
The selected renderer writes one coherent report to stderr.
An untranslated error goes through a sanitized fallback rather than fmt.Fprintln(err).
GH_DEBUG appends a redacted technical block containing transport facts.
Translators must not classify errors by matching rendered Error() strings.
Migration plan
Phase 1: establish the contract
Add diagnostic models, collector, renderers, and environment resolution.
Add --error-format=json and GH_ERROR_FORMAT.
Add exit 3 and exit 5.
Document stdout, stderr, prose-stability, and schema guarantees.
Add renderer and schema contract tests.
Phase 2: central generic translation
Move DNS, TLS, authentication, SAML, scope, rate-limit, and service handling into typed translators.
Add the sanitized fallback.
Preserve technical defaults for gh api.
Ensure remediation is host- and credential-aware.
Phase 3: migrate representative workflows
Start with the scenarios represented by existing reports:
Missing or inaccessible pull requests.
pr create multi-cause validation.
Project scope and permission failures.
issue create deferred-update partial success.
Merge conflicts and service failures.
Mutation timeouts with indeterminate outcomes.
Remove brittle string comparisons encountered in those paths before changing their rendered text.
Phase 4: broader migration and API metadata
Migrate remaining commands incrementally.
Where the API does not expose enough structured information, pursue targeted metadata improvements such as:
Required permissions or scopes.
Request IDs.
Documentation URLs.
Typed retryability or service-state information.
These improvements should not block the client-side foundation.
Acceptance criteria
Domain commands do not lead with GraphQL: or HTTP <status>: when a typed translation is available.
Untranslated domain-command errors pass through a sanitized fallback.
gh api may retain relevant transport detail.
Human diagnostics use error:, warning:, or notice: with optional note: and help:.
stdout remains reserved for command results.
TTY and non-TTY output have the same semantics.
JSON mode emits one versioned, ANSI-free envelope on stderr.
Successful commands can emit structured warnings while exiting 0.
Partial success preserves the created resource, reports the failed action, and exits 3.
Unconfirmed remote mutations use indeterminate, recommend checking before retrying, and exit 5.
Remediation commands are emitted only when known to apply to the host and credential source.
Missing and inaccessible resources remain intentionally ambiguous.
Request IDs are machine-readable and shown to humans only when support-relevant.
Debug output is redacted and contains no credentials.
Suppression requires typed evidence that an error is cascading.
Renderer, translator, schema, stream, and exit-status behavior have contract tests.
Human prose is documented as unstable; no legacy-text mode is introduced.
Inspiration, not emulation
The closest precedents are other networked operational CLIs:
kubectl translates API errors into domain language and preserves multiple validation causes.
Stripe CLI adapts recovery guidance to the active credential and profile.
AWS CLI and Heroku expose structured failure and support-correlation information.
Elm and Rust provide useful diagnostic principles:
Elm treats an error as an interaction with a person rather than an exception dump.
Rust separates diagnosis, context, and help; suppresses proven cascades; and classifies suggestion applicability.
gh is not a compiler. It performs authenticated, remote, stateful operations. Inputs may already have produced side effects, and network failures can leave outcomes unknown. This proposal does not copy source spans, file locations, compile-pass batching, type-system tutorials, or compiler-style NDJSON.
A granular stable error-code registry and documentation index.
Treating human stderr as a compatibility contract.
Reformatting third-party extension errors.
Hiding transport details from gh api.
Error telemetry without a separate privacy review.
Additional context
The repository already has strong local examples to build on: DNS translation, credential-aware 401 recovery, SAML authorization links, and some scope suggestions. This proposal is intended to make those patterns coherent across gh, not replace them with compiler-specific UX.
Describe the feature or problem you’d like to solve
ghfrequently exposes implementation details from the API client instead of explaining the user’s failed goal:The problem is larger than the
GraphQL:prefix. Today, errors may:This behavior is inconsistent.
ghalready has better patterns for some DNS, authentication, SAML, and OAuth-scope failures. The goal should be to generalize those patterns rather than fix errors command by command.Existing reports
#12596 and its revert in #12915 illustrate why changing strings alone is insufficient. The improved message depended on parsing server prose, broke an internal string comparison, and recommended
gh auth refreshfor credentials that could not necessarily be refreshed.Proposed solution
Introduce a shared diagnostic model with:
Design principles
An excellent
ghdiagnostic should:GH_DEBUG, not by default.Human diagnostic grammar
Centrally rendered diagnostics should use consistent roles:
The headline severity is one of
error,warning, ornotice.note:andhelp:are optional.Human messages should be neutral and non-blaming. TTY detection may change color and layout, but not meaning.
Request IDs should appear in structured output whenever available. Human output should show them only when contacting support or diagnosing a service failure is relevant.
gh apiis an intentional exception to transport-neutral output. Its users selected an API-level command, so HTTP and GraphQL details can remain relevant. Extensions should continue to own their stderr.Example diagnostics
These are illustrative. A translator should only assert facts the client can establish.
1. Missing or inaccessible resource
Before:
After:
2. Multiple invalid inputs
Before:
After:
Independent problems remain visible. A command-level summary avoids arbitrarily declaring one of them the primary cause.
3. Credential-aware permission remediation
For a refreshable token:
For
GH_TOKEN:A runnable command should only be emitted when it is known to apply. GitHub.com, GHES, Actions tokens, GitHub App tokens, environment tokens, and credentials managed by
ghcan require different recovery steps.4. Partial success
If the issue was created but its type could not be set:
stdout
stderr
The process exits 3. The successful resource remains on stdout, and structured stderr repeats a minimal reference to it.
This avoids recovery instructions that would create a duplicate issue.
5. Internal service failure
Before:
After:
Transport type, API path, and redacted raw facts remain available under
GH_DEBUG.6. Indeterminate remote outcome
A network timeout does not always mean that a mutation failed. The server may have completed it before the connection was lost.
The process exits 5. This is distinct from both failure and partial success.
Non-fatal diagnostics
Warnings and notices do not imply failure:
The result remains on stdout and the process exits 0.
Structured diagnostics
Add a global format independent of command-specific
--jsonoutput:The envelope is written to stderr. Normal command results remain on stdout.
One envelope is emitted per invocation rather than compiler-style diagnostic streaming:
{ "schemaVersion": 1, "outcome": "partial_success", "command": ["issue", "create"], "diagnostics": [ { "severity": "error", "category": "authorization", "message": "Issue #123 was created, but its type could not be set.", "context": { "action": "set", "resource": "issue_type" }, "causes": [ { "message": "The active account cannot set issue types in OWNER/REPO." } ], "remediation": [ { "kind": "instruction", "description": "Update the issue after obtaining the required repository permission.", "applicability": "manual" } ], "support": { "requestId": "ABC1:1234" } } ], "result": { "resources": [ { "type": "issue", "identifier": "123", "url": "https://github.com/OWNER/REPO/issues/123" } ] }, "rendered": "error: issue #123 was created, but its type could not be set\n..." }Stability
Stable in schema version 1:
Explicitly unstable:
messagedescriptionrenderedThese remain human prose and must not be parsed by automation.
Outcomes and exit statuses
The diagnostic outcome for an authentication error remains
failure; exit 4 preserves the existing shell-level distinction.Recovery categories
Version 1 uses a deliberately broad closed set:
usageauthenticationauthorizationvalidationnot_foundconflictrate_limitnetworkserviceunknownThese describe recovery classes rather than GraphQL or HTTP implementation details. A granular stable error-code registry can be considered separately.
Remediation actions
Remediation entries have a typed
kindsuch as:commanddocumentationinstructionThey also declare applicability:
exact: safe and directly runnable.conditional: applies if a stated condition is true.manual: requires user judgment or external changes.Commands should be represented as argument arrays rather than shell strings.
Proposed Go architecture
Suggested package layout:
Core interfaces would be approximately:
A diagnostic error returned by a command should preserve its underlying cause through
Unwrap, while carrying semantic context and an optional partial result.cmdutil.Factoryshould expose an injecteddiagnostics.Sink. Migrated commands use it instead of immediately printing warnings. This allowsinternal/ghcmdto combine non-fatal diagnostics with a returned error and render exactly one structured envelope.Error flow
api/preserves structured HTTP, GraphQL, header, and request metadata without choosing human prose.fmt.Fprintln(err).GH_DEBUGappends a redacted technical block containing transport facts.Translators must not classify errors by matching rendered
Error()strings.Migration plan
Phase 1: establish the contract
--error-format=jsonandGH_ERROR_FORMAT.Phase 2: central generic translation
gh api.Phase 3: migrate representative workflows
Start with the scenarios represented by existing reports:
pr createmulti-cause validation.issue createdeferred-update partial success.Remove brittle string comparisons encountered in those paths before changing their rendered text.
Phase 4: broader migration and API metadata
Migrate remaining commands incrementally.
Where the API does not expose enough structured information, pursue targeted metadata improvements such as:
These improvements should not block the client-side foundation.
Acceptance criteria
GraphQL:orHTTP <status>:when a typed translation is available.gh apimay retain relevant transport detail.error:,warning:, ornotice:with optionalnote:andhelp:.indeterminate, recommend checking before retrying, and exit 5.Inspiration, not emulation
The closest precedents are other networked operational CLIs:
kubectltranslates API errors into domain language and preserves multiple validation causes.Elm and Rust provide useful diagnostic principles:
ghis not a compiler. It performs authenticated, remote, stateful operations. Inputs may already have produced side effects, and network failures can leave outcomes unknown. This proposal does not copy source spans, file locations, compile-pass batching, type-system tutorials, or compiler-style NDJSON.Out of scope
gh pr create: improve validation and error messaging for invalid ref inputs #12889 already covers much of that work.gh api.Additional context
The repository already has strong local examples to build on: DNS translation, credential-aware 401 recovery, SAML authorization links, and some scope suggestions. This proposal is intended to make those patterns coherent across
gh, not replace them with compiler-specific UX.