Read-only mode for gh via GH_READ_ONLY env var#13621
Conversation
|
Thanks for your pull request! This is a large change (476 lines across 8 files) that doesn't reference a Large feature PRs require prior discussion in an issue before implementation — this helps the team assess whether the feature aligns with the project's direction before significant effort is invested. Please open an issue to discuss this feature first. This PR will be automatically closed in 2 days if requirements are not met. Full contribution requirements
|
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a GH_READ_ONLY environment flag that enforces a read-only mode across gh by blocking write operations (REST non-GET/HEAD, GraphQL mutations, and git push) while updating CLI help text and adding tests.
Changes:
- Document
GH_READ_ONLYin root help topics andgh apihelp. - Introduce
internal/ghenv.ReadOnly()and enforce read-only mode in the API HTTP client via transport middleware. - Block
git pushwhen read-only mode is enabled, and add coverage for git and API behavior.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/cmd/root/help_topic.go | Documents the new GH_READ_ONLY env var in global help topics. |
| pkg/cmd/api/api.go | Documents GH_READ_ONLY for the gh api command help text. |
| internal/ghenv/ghenv.go | Adds ReadOnly() helper for interpreting GH_READ_ONLY. |
| internal/ghenv/ghenv_test.go | Adds tests for ReadOnly() parsing behavior. |
| api/http_client.go | Wraps transports with read-only middleware and implements GraphQL mutation detection. |
| api/read_only_test.go | Adds tests ensuring read-only middleware/client blocks writes and preserves request bodies. |
| git/client.go | Blocks git push via AuthenticatedCommand when read-only mode is enabled. |
| git/client_test.go | Adds tests verifying read-only behavior for push/fetch paths. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| {value: "no", want: false}, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.value, func(t *testing.T) { |
| if !ok { | ||
| return false | ||
| } | ||
| return !slices.Contains([]string{"false", "0", "no", ""}, value) |
| switch strings.ToUpper(req.Method) { | ||
| case http.MethodGet, http.MethodHead: | ||
| return nil | ||
| } |
| defer req.Body.Close() | ||
| body, err := io.ReadAll(req.Body) |
Read-only mode for
ghviaGH_READ_ONLYenv varContext: I did this during a recent internal hackathon among the open source folks at Posit, where I work. I've read the contributing guidelines and realize this issue is neither marked as
help wanted, nor does it have acceptance criteria yet! So I know this PR is unlikely to be merged. I share it as a working implementation of a safety feature related to discussion in #12522 and #12624. This was done with the help of Claude (I've never written Go), with strong human oversight.What it does
Adds an opt-in read-only mode, enabled by setting the
GH_READ_ONLYenvironment variable to something truthy. When set,ghcan read and inspect GitHub resources, but cannot mutate anything on the server. This affects work via the REST and GraphQL APIs and anygit pushthatghperforms (such as ingh pr create). Blocked operations fail before anything reaches GitHub, with a non-zero exit status. This would allow a conservative user to give broad permission forgh *, including thegh apiraw passthrough, knowing that nothing will be modified on the server.In terms of HTTP calls, you can't just block everything other than GET, since GitHub's GraphQL API uses POST for every operation. Therefore the guard inspects the request body to distinguish a query (allowed) from a mutation (blocked).
Test drive
gh-with-readonlyis my localghbuild from this branch.By default, it works just like
gh:With
GH_READ_ONLYset, you can still read via GraphQL. However, you can't change things on the server, e.g. you can't star a repo.Temporarily unset
GH_READ_ONLY, so we can star a repo:Confirm the star exists and try (and fail) to delete it:
ghshim to intercept agentsHow to make sure agents use
ghin read-only mode?Proof of concept: I place a
ghshim that looks for the env vars set by agents (e.g.AGENT,CLAUDECODE,GEMINI_CLI,CURSOR_AGENT) and executes such calls withGH_READ_ONLYset. This is not part of the PR, it's just so I can exercise the feature with local agents.Otherwise, the shim falls back to stock
gh(not shown).When I direct Claude Code to do things like create an issue or label, it is unable to do so:
Notes
api.NewHTTPClientandgit.Client.AuthenticatedCommand) seems like the most efficient way to get read-only behavior for all commands, given that theghsurface doesn't make it easy to distinguish reads from writes.gh. Local writes (e.g.gh config set) are unaffected, as is agit pushoutside ofgh.