Repository navigation
Design: make gh errors human-first, actionable, and machine-readable #14038
Description
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.
Existing reports
| Area | Reports |
|---|---|
| Raw API terminology | #1686, #2756 |
| Multi-cause and input validation | #2129, #2691, #3224, #12889 |
| Permission and scope diagnostics | #6274, #8326, #8434, #8885, #9380, #12585 |
| Failed scope-message migration | #12596, reverted by #12915 |
| Partial success reported as failure | #13804, #13899 |
| Transient state conflicts | #8092 |
| Warning treated as failure | #11992 |
| Recovery behavior | #10889 |
| GraphQL client architecture | #5163 |
| Agent-friendly interfaces | #12522 |
#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 45
GraphQL: Could not resolve to a PullRequest with the number of 45.After:
$ gh pr checkout 45
error: could not find pull request 45 in cli/cli
note: the pull request may not exist or your account may not have access
help: 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/feature
error: 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 repository
help: use `--base main` and push the head branch before retryingIndependent 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:
gh pr create --error-format=json
GH_ERROR_FORMAT=json gh pr createThe 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:
messagedescriptionrendered
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:
usageauthenticationauthorizationvalidationnot_foundconflictrate_limitnetworkserviceunknown
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:
commanddocumentationinstruction
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
Core interfaces would be approximately:
type Sink interface {
Add(Diagnostic)
}
type Translator interface {
Translate(error, Environment) (Diagnostic, bool)
}
type Renderer interface {
Render(io.Writer, Report) error
}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_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
- Add diagnostic models, collector, renderers, and environment resolution.
- Add
--error-format=jsonandGH_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 createmulti-cause validation.- Project scope and permission failures.
issue createdeferred-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:orHTTP <status>:when a typed translation is available. - Untranslated domain-command errors pass through a sanitized fallback.
gh apimay retain relevant transport detail.- Human diagnostics use
error:,warning:, ornotice:with optionalnote:andhelp:. - 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:
kubectltranslates 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.
Out of scope
- Changing automatic retry behavior.
- Comprehensive client-side validation;
gh pr create: improve validation and error messaging for invalid ref inputs #12889 already covers much of that work. - 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.

Activity
cli-triage commentedon Aug 1, 2026
This is a well-structured design proposal for systematically improving
gh's error messages to be human-first, actionable, and machine-readable. It covers diagnostic modeling, structured JSON output, credential-aware remediation, partial success handling, and a phased migration plan. No duplicate issues were found that cover this scope at this level of abstraction (several referenced issues like #12522, #12889, and #9403 are cited as prior art and related work, not duplicates).Suggesting
enhancementas this is a design/feature proposal, not a bug report.williammartin commentedon Aug 6, 2026
At surface reading I think this is a good proposal. It drives me nuts that errors weren't originally treated as something for people to easily understand and act upon as a first class principle.
My main concern is that error messages are a form of pseudo contract and there's probably a lot of scripts out there parsing error messages. Many cases could probably be resolved by including the previous message for partial matches, but in the worst case there are probably scripts with exact match semantics.
maxbeizer commentedon Aug 6, 2026
Yeah. I agree that this is a great concern to raise and we simply can't break everything for everyone. I wonder if maybe this could be part of a v3.0 with a upgrade announcement, given I'm currently running 2.97
nobe4 commentedon Aug 6, 2026
I like this. ✨
✨ I like the help section, tho I'm not sure why it would be different from note.
❓ I am wondering how you would handle multiline comments there?
✨ I like the stderr shoutout, IMHO, every error, note, and help should go there.
ℹ️ I would make sure that any data is fully present, e.g. no #123 but the full URL so that it's click-able and parseable.
❓ Would it be also possible (unless it's out of scope) to include a specific sysexit code?
That would allow to also use
$?as a condition.❓ Similarly, a
code: 1234with a link to a specific failure reason/link to a doc could be really useful for debugging faster. A la shellcheck.github-actions commentedon Aug 7, 2026
Thank you for your issue! We have categorized it as an enhancement (feature or improvement) request, and it has been added to our backlog. In doing so, we are not committing to implementing this feature at this time, but, we will consider it for future releases based on community feedback and our own product roadmap.
Unless you see the help wantedContributions welcome
label, we are not currently looking for external contributions for this feature.
If you come across this issue and would like to see it implemented, please add a thumbs up! This will help us prioritize the feature. Please only comment if you have additional information or viewpoints to contribute.