Skip to content

Say why a refused Keychain read can't be fixed by signing in again - #599

Merged
jeremy merged 3 commits into
mainfrom
keychain-refusal-hint
Oct 24, 2026
Merged

jeremy merged 3 commits into
mainfrom
keychain-refusal-hint

Conversation

@jeremy

@jeremy jeremy commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Why

A customer on macOS can sign in and use hey in Terminal, but inside Codex every hey command fails with read credentials from system keyring: exit status 36, and hey mcp fails Codex's startup with handshaking with MCP server failed: connection closed. The skill then tells them to run hey auth login, so they loop. Allowing hey outside Codex's sandbox with ~/.codex/rules (as #596 proposed and Support sent by hand) didn't help: codex execpolicy check said allow, and the command still got exit status 36.

Exit status 36 is errSecInteractionNotAllowed. The Keychain gives it to a process outside the Mac's logged-in desktop session, whether or not it's sandboxed. Codex 0.163+ runs every command and MCP server from a shared background server (codex app-server, started with setsid, kept running after Codex quits). If that server was started from such a session, Codex can't read the Keychain from anywhere. A rules file can't fix that, and neither can a new login or restarting Codex.

Reproduced on macOS 27 with Codex 0.164.0, against a scratch CODEX_HOME and a scratch keychain item, never the real HEY login:

Where security find-generic-password -w runs Exit
Terminal 0
Codex's seatbelt, network off 44 (go-keyring reads this as not found, so hey says "Not logged in")
Codex's seatbelt, network on 0
Over SSH (ssh localhost) 36
Codex TUI → background server restarted from an SSH session, command allowed by a prefix_rule (runs unsandboxed) 36
Same, after codex app-server daemon restart from the desktop session 0
Same SSH-started server, TUI run with codex --no-daemon 0

With the server started from SSH, the same TUI session got the customer's exact errors: hey auth status --json gave could not read authentication status: read credentials from system keyring: exit status 36, and Codex logged MCP server startup failed server_name="hey" error=handshaking with MCP server failed: connection closed. hey mcp's stderr, which Codex keeps in ~/.codex/logs_2.sqlite, said could not read stored credentials: … exit status 36. After a restart from the desktop session, hey mcp logged MCP server running on stdio tools=6 read_only=true.

What changes

  • Classify the refusal. store.loadFromKeyring marks a keyring error whose exit status is 36 as ErrKeyringRefused. The keyring's own message is kept unchanged, so existing scripts and the skill's could not read stored credentials match still work.

  • Hint instead of a login. CredentialStorageError turns a refused read into an apierr.Error. It is used by data commands, through credentialLoadError, and by hey auth status. Its code is unknown, exit 1: unchanged where the preflight read caught it. On the rarer path where an SDK request's re-read is refused, it was api / 7 before. The hint rules out a new login and names the fix:

    • run hey from Terminal on the Mac;
    • for Codex, run codex app-server daemon restart in Terminal, then restart Codex.

    Inside Codex's sandbox (CODEX_SANDBOX set), it first says to let Codex run hey outside the sandbox, then gives the restart, since the refusal comes from the session. Any other storage failure keeps today's plain error.

  • A missing login inside Codex's sandbox. On macOS 27 a sandboxed read gets exit status 44, which reads as "Not logged in". With CODEX_SANDBOX set, the hint now says the sandbox may be hiding a saved login: check hey auth status in Terminal first. Outside the sandbox it still says Run: hey auth login. This is the misdiagnosis Let Codex run hey's read commands outside its sandbox #596's verification showed.

  • Skill. It no longer tells the person to run hey auth login when hey auth status itself fails with a keyring error. It relays the error's hint and stops.

  • docs/agents.md. A short "Keychain access from agents" section covers both cases, and explains why hey mcp shows up as a closed connection.

Not done

  • hey mcp still checks the login before the MCP handshake, so a client still sees only "connection closed". Serving the error from each tool call would surface the hint in the client. That is the area Give hey mcp its own sign-in, and send a draft only on the person's confirmation #598 reworks, so it belongs there or after it.
  • No file or token fallback for agents. As in basecamp-cli#866, a plaintext copy of a rotating refresh token, or two stores holding one, creates new failures. It also wouldn't help a process the Keychain refuses.
  • hey setup codex still writes no rules file. Let Codex run hey's read commands outside its sandbox #596 was closed in favour of a hand-made ~/.codex/rules/hey.rules. Rules are still needed for sandboxed CLI commands, but they don't help with exit status 36.

Verification

  • New tests fail with the classification or the sandbox check disabled, and pass with them:
    • TestKeyringFailures/refusal, which covers a fake exit status 36 and a real *exec.ExitError from sh -c 'exit 36', with exit 1 as the control;
    • TestAKeychainRefusalSaysSigningInAgainWontHelp, through the status and access-token paths, inside and outside the sandbox;
    • TestNotLoggedInHintInsideCodexSandbox;
    • TestRequireAuthPromptsOnlyWhenInteractiveAndStyled/inside_Codex's_sandbox….
  • TestAnUnclassifiedStorageFailureCarriesNoHint pins that only a refusal is classified.
  • The existing requireAuth test now pins CODEX_SANDBOX="", so it still passes when run inside Codex.
  • The built binary over SSH, with a scratch keychain item, prints the new hint for hey auth status --json and hey search. Under Codex's seatbelt with CODEX_SANDBOX=seatbelt, hey search prints the sandbox hint, exit 3.
  • make test, make lint, fmt-check, vet, check-surface, test-sync-skills and check-release-lockstep pass locally.

macOS's security tool exits 36 (errSecInteractionNotAllowed) when the
Keychain won't hand an item to a process outside the logged-in desktop
session: one started over SSH, or by a background server launched from
there, as Codex's app-server can be. hey reported that as "exit status 36"
with no hint, and the skill then told the person to run `hey auth login`,
which writes to the same Keychain and changes nothing.

- The store marks an exit status 36 from the keyring as ErrKeyringRefused,
  keeping the keyring's own message.
- A refused read now carries a hint that rules out a new login and names
  the fix: run hey from Terminal on the Mac, or restart Codex's background
  server from there. Inside Codex's sandbox (CODEX_SANDBOX set), the hint
  says to let Codex run hey outside it.
- A missing login inside Codex's sandbox says the sandbox may be hiding a
  saved one, instead of only "Run: hey auth login".
- The skill no longer sends a person to `hey auth login` when
  `hey auth status` fails with a keyring error.
- docs/agents.md explains both cases, and why `hey mcp` shows up as a
  closed connection.
@jeremy
jeremy requested a review from a team as a code owner October 24, 2026 19:00
Copilot AI balanced review requested due to automatic review settings October 24, 2026 19:00
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-24T19:07:55.993953Z 00ed902 Manual request
🔒 Security Review ✅ Completed 2026-10-24T19:08:05.019850Z 00ed902 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI previously approved these changes Oct 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approved

The narrowly scoped diagnostic changes preserve error compatibility, include focused regression coverage, and have no identified blocking issues.

No open issues 🎉

What changed in this PR

Improves hey-cli’s authentication diagnostics so agents distinguish refused Keychain access from a missing login, avoiding repeated sign-in attempts that cannot fix the problem.

Changes:

  • Classifies Keychain exit status 36 while preserving existing error messages and exit codes.
  • Adds recovery hints for refused reads and missing credentials inside Codex’s sandbox, with regression coverage.
  • Updates agent guidance and documentation to explain recovery and MCP startup failures.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File Description
skills/​hey/​SKILL.md Relays storage-error hints instead of recommending another login.
internal/​cmd/​root.go Uses sandbox-aware guidance for missing credentials.
internal/​cmd/​root_test.go Covers sandbox hints and isolates sandbox environment state.
internal/​cmd/​auth.go Adds refusal hints to authentication-status errors.
internal/​auth/​store.go Classifies Keychain refusal without changing its error text.
internal/​auth/​store_test.go Tests refusal classification with simulated and real exit errors.
internal/​auth/​auth.go Centralizes storage-error and missing-login hints.
internal/​auth/​auth_test.go Covers refusal hints and unchanged handling of other storage failures.
docs/​agents.md Documents Keychain recovery and MCP connection-closed errors.

Review effort: Balanced

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 306174085f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/auth/store.go
Comment thread internal/auth/auth.go Outdated
Exit status 36 comes from the desktop session, not the sandbox, so letting
Codex run hey outside its sandbox can't clear it on its own. Inside the
sandbox the hint now names that step first, then the restart.
@jeremy

jeremy commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

Not doing this — from our own pre-merge review (gpt-6.1-sol, high), on 3061740: it's true that a refusal hitting the SDK path now comes back as unknown / exit 1. That's the path where the preflight read succeeds and the re-read during request authentication gets exit status 36, and before this PR it came back as api / exit 7. Keeping 7 would label a local Keychain refusal as an API failure, and that invites a caller to retry HEY, which can't help. The same refusal caught by the preflight was already unknown / 1, so after this change both paths report it the same way. Other storage failures on the SDK path are unchanged. I've corrected the PR description, which said the exit status was unchanged everywhere.

@jeremy

jeremy commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 00ed90207d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 00ed90207d

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@jeremy
jeremy requested a balanced review from Copilot October 24, 2026 19:12
Copilot AI dismissed their stale review, a newer Copilot review was requested October 24, 2026 19:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approved

Static review found no blocking issues in the focused error classification, recovery hints, or regression coverage.

No open issues 🎉

Review effort: Balanced

@jeremy

jeremy commented Oct 24, 2026 •

Copy link
Copy Markdown
Member Author

Review threads: 2 resolved (1 fixed, 1 declined with the reasoning in the thread).
Review bodies: 1 finding answered (0 fixed, 1 declined). It came from our own gpt-6.1-sol review, not a GitHub review.
Declined: #599 (comment) — the availability probe reads an item that never exists, and a refused session answers that with 44, not 36 (checked over SSH).
Declined: #599 (comment) — the SDK-path exit code for a refusal moves from api/7 to unknown/1, matching the preflight path.
Not reported on the head: Codex reviewed 00ed902 clean, after one re-request. e4e61c6 changes only the skill's embed test.

@jeremy
jeremy merged commit 34a9016 into main Oct 24, 2026
26 checks passed
@jeremy
jeremy deleted the keychain-refusal-hint branch October 24, 2026 19:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants