Skip to content

Give hey mcp its own sign-in, and send a draft only on the person's confirmation - #598

Draft
jeremy wants to merge 13 commits into
mainfrom
mcp-own-credential
Draft

jeremy wants to merge 13 commits into
mainfrom
mcp-own-credential

Conversation

@jeremy

@jeremy jeremy commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Held for Jeremy as a draft: don't merge, don't arm auto-merge. It can't ship before the production HEY MCP (local) row exists, because its client id is still a TODO (below).

hey mcp gets its own least-privilege sign-in, and it can send, with a confirmation for each email. This is the hey-cli lane of haystack #9041 (the local_mcp row) and #9085 (the scoped device flow). It implements sending-safety.md D2–D6 for stdio, under the roadmap's implementation requirements.

What changes for a person

hey auth login --mcp                  # once: "HEY MCP (local)", read write send
claude mcp add hey -- hey mcp
  • Without that sign-in, hey mcp exits with Run: hey auth login --mcp. That includes a person who is signed in to the CLI or has HEY_TOKEN set. This breaks existing hey mcp registrations until the person runs the sign-in once, so it goes in the release notes.
  • hey_send / send_draft delivers a draft after the person confirms that exact email in their MCP client. A client that can't show a confirmation gets the draft's link instead.

1. hey auth login --mcp

  • Separate storage. The credential lives in its own keychain item, hey / hey::mcp::<origin>, beside the CLI's hey::<origin>. With no keychain it gets its own entry in credentials.json. Its refreshes and revocations name the local client id, never the CLI's. HEY_TOKEN never stands in for it.
  • Device flow first. It sends POST /oauth/device_authorizations with client_id and scope=read write send, and no resource, as #9085's contract specifies. It falls back to loopback authorization with the same scope and S256 PKCE on:
    • invalid_request: OAUTH_SCOPED_DEVICE_FLOW is off;
    • unauthorized_client: #9041 without #9085;
    • a 404.
      Other refusals, such as invalid_client, fail and don't fall back. The poll handles authorization_pending, slow_down (+5 s), access_denied, expired_token and invalid_scope, and sends install_id. The device code is never printed. The link and user code are sanitized before they're shown, and only an http(s) link is opened.
  • It reads the token response's scope.
    • A narrower grant is stored as granted. If send was unchecked, hey mcp serves no hey_send. If write is missing, it serves reads only.
    • An omitted scope means exactly what was requested (RFC 6749 §5.1).
    • A grant naming anything not requested is refused and nothing is stored. That covers full, which would mean the configured id is the CLI's own row. An id equal to the CLI's is refused before HEY is contacted.
  • hey auth logout --mcp revokes the grant with DELETE /oauth/tokens, sending the local client id and the refresh token, then removes the local credential. If the revocation fails, the credential is still removed, and a warning says to remove the app in Connected Apps.
  • hey auth status reports both credentials, in JSON under mcp and styled as an MCP: line. Neither form ever includes a token.
  • Client id. It resolves from HEY_MCP_CLIENT_ID, then from mcp_client_ids in the global config, keyed by origin, then from config.DefaultMCPClientID.
    • The default is "", a marked TODO, until Jeremy provisions production with bin/rails oauth:provision_local_mcp_client. Staging and dev then get the same id with CLIENT_ID=, so one constant serves every environment.
    • A repo's .hey/config.json can't set the id.

2. No fallback to the CLI credential

hey mcp never falls back, not even for reads. The reasons:

  • The CLI credential holds full. --read-only is hey mcp's own client-side rule, not HEY's. A read-only fallback still puts a full-scope token in the process the model drives. Then the server-enforced containment that justifies the local_mcp row (#9041's threat model) would rest on hey-cli's gateway filter staying correct. That filter is the long-tail code the row exists to contain.
  • A fallback would leave the old path working. People would never migrate, and the full token would stay the common case.
  • The cost is one sign-in. The refusal says exactly which one.

3. send_draft, written in hey-cli

Why it isn't imported.

So the code is in internal/mcpserver, in files the W5.1 move can lift as a unit (send.go, sendstate.go, sendbody.go, sendprompt.go, exposure.go).

How it's wired.

  • Its own tool. hey_send is a separate tool (D7) with destructiveHint and openWorldHint true. It is served only when all of these hold:
    • the credential holds read and send (expanded, so write counts as read);
    • the server isn't --read-only;
    • the threads domain is served.
  • Two rounds over MRTR, through go-sdk v1.8.0's input-requests support.
    1. The first round fetches GetMessageEdit, applies the policy, and returns an elicitation. Its HMAC-signed request state carries the draft's fingerprint, the sign-in, the origin, a 5-minute expiry and a method marker.
    2. The second round verifies that state and the person's accept with confirm: true. It then re-fetches the draft and recomputes the fingerprint. Any change fails with "the draft changed since you approved it". It re-runs the policy, then claims the draft and charges the caps in one locked update. Last, it calls SendDraft once, through a client that neither retries nor refreshes-and-resends after a 401. The token is refreshed just before the send instead. An answer with an id and no thread, where the draft's edit page is still served, is reported as not sent, as mail.ConfirmDelivered reads it. Every delivered field is written from the verified snapshot, and HEY replaces the whole recipient set when addressed is present (Entry::Addressed#address).
    • A 2026-07-28 client carries the rounds itself. An older client gets server-initiated elicitation/create from the SDK shim. Both are tested.
  • The fingerprint covers:
    • draft id, sender id and address;
    • sorted To, Cc and Bcc;
    • subject;
    • the attachments (sgid, name, size, type, caption);
    • a sha256 of the body as delivered, each attachment rebuilt from its identity, so signed URLs don't count;
    • mode now.
  • The prompt follows D2's layout. Every recipient field is shown, Bcc included, as "—" when empty. Recipients are shown as "Name" <address>. Controls, line separators and every invisible character (all Unicode format characters, which include bidi, zero-width and tag characters, plus variation selectors and private-use characters) are escaped as \u{…}. Header fields are single-line. Delivered ones (the subject, attachment names and captions) are shown whole, and only display names are capped. The body is indented, and the body shows at most 1,500 characters with a link to the rest. New recipients are marked (new: never emailed before).
  • Without elicitation (D3), send_draft returns a non-error sent: false result with the draft's edit link. A decline or cancel gets the same result. There is no "I confirm" parameter.

Single use and never retried. The claim key is origin#draft_id and nothing else. It lasts 24 hours and is never released, even when HEY refuses. A replayed state, a second confirmation of the same draft, and a resend after a failure are all refused, and in each case SendDraft is called once.

D4.

  • Caps: at most 20 recipients per message, and per sign-in at most 50 sends and 10 unique new addresses in a rolling 24 hours. A send is charged only for new addresses not already in the window.
  • "Known" means mail this identity sent to the address before the sign-in's GrantedAt. It is looked up through advanced search (refine[to]), and each match is verified locally against the entry's creator (one of the identity's addresses), its delivery time (active_at, not created_at, so a draft made before the sign-in and sent after it doesn't count) and its recipients. So a loose search can't make an address known. A failed lookup counts as new. Contact records never count.
  • The principal is the sign-in's local GrantKey, minted at login and stored with the credential.

D2's unseen-content and remote-reference rules.

  • To anyone new, or while exposed, the draft goes as the plain text the confirmation shows. The body sent is built from that text alone (lines escaped, joined with <br>), with trailing whitespace and invisible characters dropped. So no markup choice (hidden text, a list's type, bold against italic, a break against a paragraph) can carry something the person didn't read. Such a draft must fit the 1,500-character prompt whole and have no attachment. The plain/as-is decision is signed into the confirmation, and a change between rounds refuses the send. To known recipients on an unexposed sign-in, the draft goes as it is. This replaced a lossless-markup classifier whose every edge was a new hidden channel.
  • A body with something a mail client loads by itself is refused for every recipient. That covers img, iframe, link, style, svg and the like, src/srcset/background and similar attributes, and any style with an escape or a (, which Trix never writes.
  • An attachment is delivered as its identity alone. Each one is rebuilt from its sgid, filename, size, type and caption, with nothing inside it, which is what Action Text keeps of one anyway. The body sent is exactly the body hashed, so nothing in an attachment's wrapper (a preview URL, a caption's markup, an attribute) goes out or needs checking. An attachment with no sgid or filename, or with embedded content (Trix's filename-less embedded HTML bodies), is refused for every recipient. Unverified against a live HEY: that UpdateMessage accepts the rebuilt, childless figure as Trix's own submission.
  • The body is parsed with scripting off. noscript, template and the raw-text fallbacks are refused as loading markup.
  • A draft that is already scheduled is refused. send_at isn't implemented.

D6 (exposure).

  • The gateway serves HEY raw, and GetTopic's screened_in isn't in the hey-sdk 0.33.1 model. So readExposes classifies every served read by what it can return.
    • Box reads, identity, box groups and calendars don't expose, since postings exist only for screened-in threads.
    • Search, threads, messages, drafts, reply and forward prefills, contacts, the Screener and calendar recordings do. So does any write that answers with a body (reveal_contact returns the contact).
    • An unclassified read exposes. A test holds the table total over the served catalog.
  • The marker is written before the content is returned, and a read whose marker can't be written is refused. It's recorded whenever the sign-in can send, including from a --read-only or narrowed hey mcp, since another process on the same sign-in may send.
  • A process stays on the sign-in it started with. Its auth manager is pinned to that sign-in's key, so a later hey auth login --mcp is refused rather than adopted. A refresh that narrows the grant below sending refuses sends and exposing reads until the process restarts.
  • While a sign-in is exposed (10 minutes, restarted by each exposing read), drafts to anyone new are refused. The claim re-reads exposure under the lock, so a marker that lands between the prompt and the approval still refuses.

State on stdio (the roadmap's invariants row) is $XDG_STATE_HOME/hey-cli/mcp-send-state.json. It holds claims, send times, new-address membership and exposure.

  • It is read, checked and written under an exclusive flock on a separate mcp-send-state.lock that is never renamed, with a 5-second timeout that fails closed.
  • Each write goes to a temp file that is fsynced, renamed into place, and then the directory is fsynced. A failed fsync fails the claim, and nothing is sent.
  • The confirmation key, mcp-confirm.key, is generated once per installation under the same lock.

Help and docs. hey mcp --help, hey auth login/logout --help, docs/agents.md ("Its own sign-in", "Drafts first, sends confirmed"), docs/cli.md, the environment help topic (HEY_MCP_CLIENT_ID), and the README's one MCP line, whose example no longer ran without the new sign-in. .surface gains --mcp on login and logout.

Tests

make check is green: fmt-check, vet, golangci-lint, the unit tests, sync-skills and tidy-check. The new and changed tests cover the brief's six cases and more:

Asked for Tests
login stores a separate credential TestMCPLoginStoresACredentialApartFromTheCLIs (two keychain items, the CLI's untouched), …InThePlaintextFallbackToo, TestAuthLoginMCPKeepsTheCLIsCredentialAndStatusShowsBoth (through the command, with status and logout)
hey mcp refuses with no MCP credential TestMCPCommandRefusesWithoutItsOwnCredential (CLI signed in and HEY_TOKEN set; nothing reaches HEY), TestMCPManagerNeverUsesHEYToken
the scope response is honored TestMCPLoginHonorsTheScopeHEYGranted (all, send unchecked, omitted), TestMCPLoginRefusesAScopeItDidntAskFor (full), TestMCPCommandServesWhatTheGrantAllows, TestMCPRefreshNamesTheLocalClientAndKeepsTheScope
send_draft needs a confirmed elicitation TestSendDraftSendsOnlyWhatThePersonConfirmed (both protocol eras), TestSendDraftWithoutApprovalSendsNothing (decline, cancel, unticked)
digest mismatch and replay refused TestSendDraftRefusesADraftChangedAfterApproval (Bcc added, recipient swapped, subject, body, sender, schedule), TestAnAttachmentReplacedWithTheSameNameAndSizeIsAChange, TestAReplayedConfirmationSendsNothing, TestTwoConfirmationsOfOneDraftSendItOnce, TestSendDraftSendsADraftOnce, TestAForgedOrExpiredConfirmationSendsNothing
a host without elicitation can't send TestSendDraftCantSendFromAHostWithoutElicitation (both eras)

Also covered: caps (TestSendDraftCapsRecipientsSendsAndNewAddresses), known recipients (TestSendDraftFlagsANewRecipient, TestMailSentAfterTheSignInDoesntMakeARecipientKnown), exposure (TestAnUnscreenedReadRefusesSendsToNewRecipients, TestABoxReadDoesntExpose, …BetweenPromptAndApproval…, TestTheClaimRereadsExposure, TestAnExposedReadIsRefusedWhenItsMarkerCantBeWritten, TestEveryServedReadIsClassifiedForExposure), unseen content and remote references, inert rendering, durability (TestAFailedFsyncFailsTheSendClosed, TestEitherFailedFsyncFailsTheClaim, TestClaimsAndCapsSurviveARestart), caps charged in the claim itself (TestTheClaimEnforcesTheCaps), client id resolution (TestMCPClientIDResolution), never retried (TestAFailedSendIsNeverRetriedAndStaysClaimed), and an end-to-end run through hey mcp (TestMCPCommandSendsAConfirmedDraftOnce).

Revert proof: in the latest revert-proof comment: 45 of 45 pieces fail their tests when taken out.

Not verified against a live HEY

  • The known-recipient lookup depends on advanced search returning a match's entries with addressed, creator and created_at. If HEY's summaries leave addressed out, every recipient reads as new. That fails closed, but it's strict: no unseen content goes anywhere, and the 10-new-address cap applies to everyone.
  • Attachment sgid stability across two GetMessageEdit fetches. If the sgid isn't stable, a draft with an attachment always fails as "changed". That also fails closed.
  • The device flow against #9085 on staging, and the loopback consent with send unchecked under #9035.

What haystack and hey-mcp-server still need

  • haystack:
    • Provision HEY MCP (local) in production, then set config.DefaultMCPClientID, then staging and dev with CLIENT_ID=.
    • Turn on OAUTH_SCOPED_DEVICE_FLOW per host (until then the login falls back to loopback).
    • A send-history answer that doesn't depend on search summaries ("has this identity sent to X before T").
    • screened_in in the SDK model (GetTopic, haystack#9083), so the exposure table can read screening per response rather than per operation.
  • hey-mcp-server:
    • W5.1 (Fluent text input: positional args, short flags, and stdin #14) plus a way for a public module to depend on it. When that lands, these files move there and hey mcp imports them.
    • Its stdio gateway refuses clearance writes other than screen-out (D5); hey-cli's still serves them. That's out of scope here, and it doesn't open sends, because every Screener and thread read exposes regardless.
    • The scopes.md edits #9085 lists.
  • hey-sdk: the behavior model still marks UpdateMessage idempotent with retries (D2.4). send_draft goes through hey-cli's no-retry client, as every hey mcp mutation does.

…onfirmation

hey auth login --mcp signs hey mcp in as HEY MCP (local) (haystack#9041) on
read write send: by the scoped device flow where HEY serves it (#9085), and by
loopback otherwise. The credential is stored apart from the CLI's, refreshes
and revokes as the local client, reads the scope HEY granted, and HEY_TOKEN
never stands in for it. hey mcp serves on it alone and refuses without it.

hey_send's send_draft delivers a draft after the person confirms that exact
email through MCP elicitation (sending-safety D2-D6): digest-bound, single
use, never retried, with the recipient caps and the exposure window, and its
state in a locked, fsynced file. A host without elicitation gets the draft's
link instead.
@jeremy

jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

Revert proof on 3f8aa2f. I took out each piece, ran the tests that hold it, and restored the file from HEAD. All 16 of the 16 pieces fail their tests when they are taken out, and make check is green with them in.

Piece taken out Fails Failing tests
hey mcp's credential shares the CLI's storage key yes TestMCPLoginStoresACredentialApartFromTheCLIs, TestMCPLoginStoresApartInThePlaintextFallbackToo
hey mcp falls back to the CLI's credential yes TestMCPCommandRefusesWithoutItsOwnCredential
hey mcp's manager honors HEY_TOKEN yes TestMCPManagerNeverUsesHEYToken
the granted scope is ignored (requested scope stored) yes TestMCPLoginHonorsTheScopeHEYGranted, TestMCPLoginRefusesAScopeItDidntAskFor
refresh names the CLI's client yes TestMCPRefreshNamesTheLocalClientAndKeepsTheScope
any answer to the elicitation counts as approval yes TestSendDraftWithoutApprovalSendsNothing
no fingerprint check on the second round yes TestSendDraftNamesTheChangeWhenOnlyTheContentMoved, TestSendDraftRefusesADraftChangedAfterApproval
attachments hashed with their signed URLs yes TestAnAttachmentReplacedWithTheSameNameAndSizeIsAChange
no per-draft claim yes TestAFailedSendIsNeverRetriedAndStaysClaimed, TestAReplayedConfirmationSendsNothing, TestClaimsAndCapsSurviveARestart, TestSendDraftSendsADraftOnce, TestTwoConfirmationsOfOneDraftSendItOnce
a host without elicitation is asked anyway yes TestSendDraftCantSendFromAHostWithoutElicitation
reads never mark exposure yes TestAnExposedReadIsRefusedWhenItsMarkerCantBeWritten, TestAnUnscreenedReadRefusesSendsToNewRecipients
the claim doesn't re-read exposure yes TestTheClaimRereadsExposure
no new-address cap yes TestTheClaimEnforcesTheCaps
a failed fsync of the state file is ignored yes TestEitherFailedFsyncFailsTheClaim
a failed fsync of the directory is ignored yes TestEitherFailedFsyncFailsTheClaim
the claim doesn't refuse unseen content while exposed yes TestTheClaimRereadsExposure

@jeremy

jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

@codex review

@jeremy
jeremy requested a balanced review from Copilot October 23, 2026 22:38
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 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-24T01:25:53.435128Z 0c62b58 Manual request
🔒 Security Review 🔄 Running since 2026-10-24T01:22:35.936864Z 0c62b58 Manual request

Security findings

Advisory findings (6)

ℹ️ 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.

Comment thread internal/mcpserver/server.go Fixed

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.

🟡 Changes recommended

Unresolved confirmation-integrity, grant-bound exposure, and rolling recipient-limit gaps weaken the sending safeguards.

6 open issues
What changed in this PR

This PR gives hey mcp a separate, least-privilege sign-in and adds draft sending through explicit confirmation in the MCP client. Production rollout remains gated on provisioning the local MCP client ID.

Changes:

  • Adds isolated MCP credentials, device-flow login with browser fallback, revocation, and status reporting.
  • Introduces hey_send with draft-bound confirmation, persistent send claims, recipient limits, and exposure tracking.
  • Updates command help, documentation, and tests for the new authentication and sending behavior.

[!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
README.md Updates MCP setup and sending guidance.
internal/​mcpserver/​server.go Registers sending and exposure tracking.
internal/​mcpserver/​sendstate.go Persists claims, limits, exposure, and confirmation keys.
internal/​mcpserver/​sendprompt.go Formats send confirmations and escapes fields.
internal/​mcpserver/​sendbody.go Analyzes bodies, attachments, and remote references.
internal/​mcpserver/​send.go Implements confirmed draft delivery and policy checks.
internal/​mcpserver/​send_test.go Tests confirmation, limits, exposure, and durability.
internal/​mcpserver/​exposure.go Classifies reads for exposure tracking.
internal/​mcpserver/​drafts.go Updates draft-only messaging to reference confirmed sending.
internal/​mcpserver/​domains.go Updates the threads tool description.
internal/​mcpserver/​dispatch.go Records exposure before returning read results.
internal/​config/​mcp.go Resolves the MCP client ID from trusted configuration.
internal/​config/​mcp_test.go Tests client ID resolution and validation.
internal/​cmd/​sdk.go Removes the superseded shared-client helper.
internal/​cmd/​root.go Initializes MCP authentication separately.
internal/​cmd/​mcp.go Uses MCP credentials and configures sending.
internal/​cmd/​mcp_test.go Tests credential isolation, tool availability, and sending.
internal/​cmd/​mcp_auth.go Implements MCP login, logout, and status handling.
internal/​cmd/​mcp_auth_test.go Tests MCP authentication commands.
internal/​cmd/​help_topics.go Documents MCP authentication environment settings.
internal/​cmd/​auth.go Adds MCP login/logout flags and status output.
internal/​auth/​store.go Stores scope, client, and grant metadata.
internal/​auth/​oauth.go Captures granted scopes in token responses.
internal/​auth/​mcp.go Defines isolated MCP authentication and scope validation.
internal/​auth/​mcp_test.go Tests scoped login, refresh, fallback, and revocation.
internal/​auth/​device.go Implements device authorization and polling.
internal/​auth/​auth.go Supports separate credential keys and scoped refreshes.
docs/​cli.md Documents MCP authentication commands.
docs/​agents.md Explains MCP sign-in and confirmed sending safeguards.
.surface Records MCP flags on login and logout commands.

Review effort: Balanced

Comment thread internal/cmd/mcp.go Outdated
Comment thread internal/mcpserver/send.go Outdated
Comment thread internal/mcpserver/sendbody.go Outdated
Comment thread internal/mcpserver/sendprompt.go Outdated
Comment thread internal/mcpserver/server.go Outdated
Comment thread internal/mcpserver/sendstate.go Outdated

@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: 3f8aa2fc58

ℹ️ 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/config/mcp.go
Comment thread internal/mcpserver/sendbody.go Outdated
Comment thread internal/mcpserver/server.go Outdated
Comment thread internal/cmd/mcp_auth.go Outdated
- Record exposure whenever the sign-in can send, including from a read-only
  or narrowed hey mcp, since another process on the same sign-in may send.
- Refuse sends and exposing reads once the stored sign-in no longer matches
  the one the process started on.
- Measure the body against the cap as the prompt shows it, escapes included.
- Refuse attachments that aren't plain blobs: no sgid or filename, embedded
  content, stray attributes or markup inside.
- Show every address in full in the confirmation.
- Keep a new address in the rolling window for 24 hours after its last send.
- Report can_send with the same predicate hey mcp serves hey_send on.
Comment thread internal/mcpserver/server.go Fixed
@jeremy

jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

Revert proof on 13c80ff, after round one's fixes. I took out each piece, ran the tests that hold it, and restored the file from HEAD. All 23 of the 23 pieces fail their tests when they are taken out, and make check is green with them in.

Piece taken out Fails Failing tests
hey mcp's credential shares the CLI's storage key yes TestMCPLoginStoresACredentialApartFromTheCLIs, TestMCPLoginStoresApartInThePlaintextFallbackToo
hey mcp falls back to the CLI's credential yes TestMCPCommandRefusesWithoutItsOwnCredential
hey mcp's manager honors HEY_TOKEN yes TestMCPManagerNeverUsesHEYToken
the granted scope is ignored (requested scope stored) yes TestMCPLoginHonorsTheScopeHEYGranted, TestMCPLoginRefusesAScopeItDidntAskFor
refresh names the CLI's client yes TestMCPRefreshNamesTheLocalClientAndKeepsTheScope
any answer to the elicitation counts as approval yes TestSendDraftWithoutApprovalSendsNothing
no fingerprint check on the second round yes TestSendDraftNamesTheChangeWhenOnlyTheContentMoved, TestSendDraftRefusesADraftChangedAfterApproval
attachments hashed with their signed URLs yes TestAnAttachmentReplacedWithTheSameNameAndSizeIsAChange
no per-draft claim yes TestAFailedSendIsNeverRetriedAndStaysClaimed, TestAReplayedConfirmationSendsNothing, TestClaimsAndCapsSurviveARestart, TestSendDraftSendsADraftOnce, TestTwoConfirmationsOfOneDraftSendItOnce
a host without elicitation is asked anyway yes TestSendDraftCantSendFromAHostWithoutElicitation
reads never mark exposure yes TestAnExposedReadIsRefusedWhenItsMarkerCantBeWritten, TestAnUnscreenedReadRefusesSendsToNewRecipients
the claim doesn't re-read exposure yes TestTheClaimRereadsExposure
no new-address cap yes TestTheClaimEnforcesTheCaps
a failed fsync of the state file is ignored yes TestEitherFailedFsyncFailsTheClaim
a failed fsync of the directory is ignored yes TestEitherFailedFsyncFailsTheClaim
the claim doesn't refuse unseen content while exposed yes TestTheClaimRereadsExposure
exposure recorded only where hey_send is served yes TestAReadOnlyServerStillRecordsExposure
no check that the sign-in is unchanged yes TestASignInChangedUnderTheServerRefusesSendsAndExposingReads
body cap measured on the raw text yes TestEscapesCountTowardTheBodyThePromptCanShow
unverifiable attachments sent yes TestAnAttachmentThatIsntAPlainBlobIsRefused
addresses truncated in the prompt yes TestTheConfirmationShowsAWholeAddressHoweverLong
the window keeps an address's first send time yes TestTheNewAddressWindowRollsWithTheLastSend
can_send ignores write yes TestMCPCanSendNeedsWriteAndSend

@jeremy

jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

@codex review

@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: 13c80ffa4f

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.

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.

Comment thread internal/auth/device.go Outdated
Comment thread internal/mcpserver/send.go
Comment thread internal/mcpserver/send.go Outdated
Comment thread internal/mcpserver/sendbody.go

@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: 13c80ffa4f

ℹ️ 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/mcpserver/sendbody.go Outdated
Comment thread internal/mcpserver/send.go
Comment thread internal/cmd/mcp.go Outdated
Comment thread internal/auth/device.go Outdated
- Report a delivery HEY kept as a draft as not sent (the redirect to the
  edit page that mail.ConfirmDelivered reads for hey draft send).
- Count a recipient known by when the mail went out, not when the draft was
  created.
- Treat an escaped or functional style as a loading reference.
- Accept an attachment's preview image only from HEY's own host, with no
  attributes but its size and alt text.
- Refuse a send when a refresh narrowed the grant below sending.
- Sanitize the device link and code before they reach the terminal, and open
  only a web link.
@jeremy

jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

Revert proof on 47dc338, after round two's fixes. I took out each piece, ran the tests that hold it, and restored the file from HEAD. All 28 of the 28 pieces fail their tests when they are taken out, and make check is green with them in.

Piece taken out Fails Failing tests
hey mcp's credential shares the CLI's storage key yes TestMCPLoginStoresACredentialApartFromTheCLIs, TestMCPLoginStoresApartInThePlaintextFallbackToo
hey mcp falls back to the CLI's credential yes TestMCPCommandRefusesWithoutItsOwnCredential
hey mcp's manager honors HEY_TOKEN yes TestMCPManagerNeverUsesHEYToken
the granted scope is ignored (requested scope stored) yes TestMCPLoginHonorsTheScopeHEYGranted, TestMCPLoginRefusesAScopeItDidntAskFor
refresh names the CLI's client yes TestMCPRefreshNamesTheLocalClientAndKeepsTheScope
any answer to the elicitation counts as approval yes TestSendDraftWithoutApprovalSendsNothing
no fingerprint check on the second round yes TestSendDraftNamesTheChangeWhenOnlyTheContentMoved, TestSendDraftRefusesADraftChangedAfterApproval
attachments hashed with their signed URLs yes TestAnAttachmentReplacedWithTheSameNameAndSizeIsAChange
no per-draft claim yes TestAFailedSendIsNeverRetriedAndStaysClaimed, TestAReplayedConfirmationSendsNothing, TestClaimsAndCapsSurviveARestart, TestSendDraftSendsADraftOnce, TestTwoConfirmationsOfOneDraftSendItOnce
a host without elicitation is asked anyway yes TestSendDraftCantSendFromAHostWithoutElicitation
reads never mark exposure yes TestAnExposedReadIsRefusedWhenItsMarkerCantBeWritten, TestAnUnscreenedReadRefusesSendsToNewRecipients
the claim doesn't re-read exposure yes TestTheClaimRereadsExposure
no new-address cap yes TestTheClaimEnforcesTheCaps
a failed fsync of the state file is ignored yes TestEitherFailedFsyncFailsTheClaim
a failed fsync of the directory is ignored yes TestEitherFailedFsyncFailsTheClaim
the claim doesn't refuse unseen content while exposed yes TestTheClaimRereadsExposure
exposure recorded only where hey_send is served yes TestAReadOnlyServerStillRecordsExposure
no check that the sign-in is unchanged yes TestASignInChangedUnderTheServerRefusesSendsAndExposingReads
body cap measured on the raw text yes TestEscapesCountTowardTheBodyThePromptCanShow
unverifiable attachments sent yes TestAnAttachmentThatIsntAPlainBlobIsRefused
addresses truncated in the prompt yes TestTheConfirmationShowsAWholeAddressHoweverLong
the window keeps an address's first send time yes TestTheNewAddressWindowRollsWithTheLastSend
can_send ignores write yes TestMCPCanSendNeedsWriteAndSend
a delivery HEY kept as a draft reported sent yes TestADeliveryHEYKeptAsADraftIsntReportedSent
known by creation time yes TestADraftMadeBeforeTheSignInButSentAfterDoesntMakeARecipientKnown
styles checked by literal url( only yes TestAnEscapedOrFunctionalStyleCountsAsLoading
any preview image accepted yes TestAnAttachmentPreviewMustComeFromHEY
device link and code shown raw yes TestDeviceLoginSanitizesWhatItShows

@jeremy

jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

@codex review

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.

Comment thread internal/mcpserver/send_test.go
Comment thread internal/mcpserver/sendbody.go Outdated

@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: 47dc338afd

ℹ️ 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/cmd/mcp.go
Comment thread internal/mcpserver/sendbody.go Outdated
Comment thread internal/mcpserver/sendbody.go
Comment thread internal/config/mcp.go
@jeremy

jeremy commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

Revert proof on b0d07de. I took out each piece, ran the tests that hold it, and restored the file from HEAD. All 36 of the 36 pieces fail their tests when they are taken out, and make check is green with them in. The three whitespace and escape-measurement rows are gone because the classifier they guarded is gone. Three rows for the plain-text path replace them.

Piece taken out Fails Failing tests
hey mcp's credential shares the CLI's storage key yes TestMCPLoginStoresACredentialApartFromTheCLIs, TestMCPLoginStoresApartInThePlaintextFallbackToo
hey mcp falls back to the CLI's credential yes TestMCPCommandRefusesWithoutItsOwnCredential
hey mcp's manager honors HEY_TOKEN yes TestMCPManagerNeverUsesHEYToken
the granted scope is ignored (requested scope stored) yes TestMCPLoginHonorsTheScopeHEYGranted, TestMCPLoginRefusesAScopeItDidntAskFor
refresh names the CLI's client yes TestMCPRefreshNamesTheLocalClientAndKeepsTheScope
any answer to the elicitation counts as approval yes TestSendDraftWithoutApprovalSendsNothing
no fingerprint check on the second round yes TestSendDraftNamesTheChangeWhenOnlyTheContentMoved, TestSendDraftRefusesADraftChangedAfterApproval
no per-draft claim yes TestAFailedSendIsNeverRetriedAndStaysClaimed, TestAReplayedConfirmationSendsNothing, TestClaimsAndCapsSurviveARestart, TestSendDraftSendsADraftOnce, TestTwoConfirmationsOfOneDraftSendItOnce
a host without elicitation is asked anyway yes TestSendDraftCantSendFromAHostWithoutElicitation
reads never mark exposure yes TestAnExposedReadIsRefusedWhenItsMarkerCantBeWritten, TestAnUnscreenedReadRefusesSendsToNewRecipients
the claim doesn't re-read exposure yes TestTheClaimRereadsExposure
no new-address cap yes TestTheClaimEnforcesTheCaps
a failed fsync of the state file is ignored yes TestEitherFailedFsyncFailsTheClaim
a failed fsync of the directory is ignored yes TestEitherFailedFsyncFailsTheClaim
the claim doesn't refuse unseen content while exposed yes TestTheClaimRereadsExposure
exposure recorded only where hey_send is served yes TestAReadOnlyServerStillRecordsExposure
no check that the sign-in is unchanged yes TestASignInChangedUnderTheServerRefusesSendsAndExposingReads
unverifiable attachments sent yes TestAnAttachmentThatIsntAPlainBlobIsRefused
addresses truncated in the prompt yes TestTheConfirmationShowsAWholeAddressHoweverLong
the window keeps an address's first send time yes TestTheNewAddressWindowRollsWithTheLastSend
can_send ignores write yes TestMCPCanSendNeedsWriteAndSend
a delivery HEY kept as a draft reported sent yes TestADeliveryHEYKeptAsADraftIsntReportedSent
known by creation time yes TestADraftMadeBeforeTheSignInButSentAfterDoesntMakeARecipientKnown
styles checked by literal url( only yes TestAnEscapedOrFunctionalStyleCountsAsLoading
device link and code shown raw yes TestDeviceLoginSanitizesWhatItShows
attachment wrappers delivered as they came yes TestAnAttachmentIsDeliveredAsItsIdentityAlone, TestAnAttachmentReplacedWithTheSameNameAndSizeIsAChange
a pinned manager adopts another sign-in yes TestAPinnedManagerRefusesAnotherSignIn
conflicting client ids for one origin accepted yes TestConflictingSpellingsOfOneOriginAreRefused
the subject truncated in the prompt yes TestTheWholeSubjectIsShown
only listed invisible characters escaped yes TestEveryInvisibleCharacterIsEscaped
writes that answer content don't expose yes TestAWriteThatAnswersWithContentExposes
origin spelled as configured yes TestOriginHasOneSpelling
the send's client may refresh and resend yes TestMCPCommandNeverResendsADeliveryAfterA401
to someone new the draft goes as it is yes TestToSomeoneNewWhatGoesIsWhatWasShown
no check that the plain path held between rounds yes TestAPlainPathThatChangesBetweenRoundsRefusesTheSend
plain text keeps invisible characters yes TestToSomeoneNewWhatGoesIsWhatWasShown

@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. Breezy!

Reviewed commit: b0d07de02c

ℹ️ 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".

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.

🟡 Changes recommended

Unresolved body normalization, exposure tracking, and refresh-ordering issues weaken the intended sending safeguards.

3 open issues
1 resolved since last review

Review effort: Balanced

Comment thread internal/mcpserver/send.go
Comment thread internal/mcpserver/sendbody.go Outdated
Comment thread internal/mcpserver/send.go
…e trimming

- Check the sign-in again after the second round's review, renewing the token
  there, so a renewal that drops send refuses before the draft is claimed. The
  send itself no longer renews.
- Remove invisible characters from plain text before trimming trailing
  whitespace, so none can shield spaces from the trim.
- Name no draft text, such as an attachment's filename, in a refusal.
@jeremy

jeremy commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

Revert proof on 9f1e95d. I took out each piece, ran the tests that hold it, and restored the file from HEAD. All 39 of the 39 pieces fail their tests when they are taken out, and make check is green with them in.

Piece taken out Fails Failing tests
hey mcp's credential shares the CLI's storage key yes TestMCPLoginStoresACredentialApartFromTheCLIs, TestMCPLoginStoresApartInThePlaintextFallbackToo
hey mcp falls back to the CLI's credential yes TestMCPCommandRefusesWithoutItsOwnCredential
hey mcp's manager honors HEY_TOKEN yes TestMCPManagerNeverUsesHEYToken
the granted scope is ignored (requested scope stored) yes TestMCPLoginHonorsTheScopeHEYGranted, TestMCPLoginRefusesAScopeItDidntAskFor
refresh names the CLI's client yes TestMCPRefreshNamesTheLocalClientAndKeepsTheScope
any answer to the elicitation counts as approval yes TestSendDraftWithoutApprovalSendsNothing
no fingerprint check on the second round yes TestSendDraftNamesTheChangeWhenOnlyTheContentMoved, TestSendDraftRefusesADraftChangedAfterApproval
no per-draft claim yes TestAFailedSendIsNeverRetriedAndStaysClaimed, TestAReplayedConfirmationSendsNothing, TestClaimsAndCapsSurviveARestart, TestSendDraftSendsADraftOnce, TestTwoConfirmationsOfOneDraftSendItOnce
a host without elicitation is asked anyway yes TestSendDraftCantSendFromAHostWithoutElicitation
reads never mark exposure yes TestAnExposedReadIsRefusedWhenItsMarkerCantBeWritten, TestAnUnscreenedReadRefusesSendsToNewRecipients
the claim doesn't re-read exposure yes TestTheClaimRereadsExposure
no new-address cap yes TestTheClaimEnforcesTheCaps
a failed fsync of the state file is ignored yes TestEitherFailedFsyncFailsTheClaim
a failed fsync of the directory is ignored yes TestEitherFailedFsyncFailsTheClaim
the claim doesn't refuse unseen content while exposed yes TestTheClaimRereadsExposure
exposure recorded only where hey_send is served yes TestAReadOnlyServerStillRecordsExposure
no check that the sign-in is unchanged yes TestASignInChangedUnderTheServerRefusesSendsAndExposingReads
unverifiable attachments sent yes TestAnAttachmentThatIsntAPlainBlobIsRefused
addresses truncated in the prompt yes TestTheConfirmationShowsAWholeAddressHoweverLong
the window keeps an address's first send time yes TestTheNewAddressWindowRollsWithTheLastSend
can_send ignores write yes TestMCPCanSendNeedsWriteAndSend
a delivery HEY kept as a draft reported sent yes TestADeliveryHEYKeptAsADraftIsntReportedSent
known by creation time yes TestADraftMadeBeforeTheSignInButSentAfterDoesntMakeARecipientKnown
styles checked by literal url( only yes TestAnEscapedOrFunctionalStyleCountsAsLoading
device link and code shown raw yes TestDeviceLoginSanitizesWhatItShows
attachment wrappers delivered as they came yes TestAnAttachmentIsDeliveredAsItsIdentityAlone, TestAnAttachmentReplacedWithTheSameNameAndSizeIsAChange
a pinned manager adopts another sign-in yes TestAPinnedManagerRefusesAnotherSignIn
conflicting client ids for one origin accepted yes TestConflictingSpellingsOfOneOriginAreRefused
the subject truncated in the prompt yes TestTheWholeSubjectIsShown
only listed invisible characters escaped yes TestEveryInvisibleCharacterIsEscaped
writes that answer content don't expose yes TestAWriteThatAnswersWithContentExposes
origin spelled as configured yes TestOriginHasOneSpelling
the send's client may refresh and resend yes TestMCPCommandNeverResendsADeliveryAfterA401
to someone new the draft goes as it is yes TestToSomeoneNewWhatGoesIsWhatWasShown
no check that the plain path held between rounds yes TestAPlainPathThatChangesBetweenRoundsRefusesTheSend
plain text keeps invisible characters yes TestToSomeoneNewWhatGoesIsWhatWasShown
no grant check right before the claim yes TestAGrantNarrowedDuringTheReviewDoesntClaimTheDraft
trailing whitespace trimmed before invisible characters go yes TestToSomeoneNewWhatGoesIsWhatWasShown
refusals echo the attachment's filename yes TestARefusalNamesNoDraftText

@jeremy

jeremy commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@codex review

@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: 9f1e95d6d2

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.

@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: 9f1e95d6d2

ℹ️ 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/cmd/mcp.go Outdated
Comment thread internal/mcpserver/dispatch.go
Comment thread internal/mcpserver/sendbody.go Outdated

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.

🔵 Needs a closer look

Unresolved sending-safety issues, pending production client provisioning, and unverified live HEY behavior require human review before approval.

1 open issue
1 previously missed issue

In code that hasn't changed since last review

Medium severity Propagate token renewal errors before claiming drafts

internal/​cmd/​mcp.go:160

If the token enters its renewal window after the final review and renewal fails transiently, discarding the AccessToken error lets the stored grant pass checkGrant. The draft is then claimed and its caps charged even if request authentication fails again before issuing a delivery PUT, blocking the draft for 24 hours without a delivery attempt. Propagate the renewal error here so the send stops before claiming.

3 resolved since last review

Review effort: Balanced

Comment thread internal/mcpserver/sendbody.go
… drop comments

- A token that can't be renewed right before the claim refuses the send, so a
  draft that certainly wasn't sent isn't claimed.
- A write HEY applied is acknowledged as applied, with its answer withheld,
  when the exposure marker can't be written, so it isn't retried.
- The plain body keeps the spacing the confirmation showed (non-breaking
  spaces for leading and repeated spaces, and tabs).
- HTML comments are never sent; a conditional comment is markup Outlook
  renders.
@jeremy

jeremy commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

Revert proof on 72f3ccc. I took out each piece, ran the tests that hold it, and restored the file from HEAD. All 42 of the 42 pieces fail their tests when they are taken out, and make check is green with them in.

Piece taken out Fails Failing tests
hey mcp's credential shares the CLI's storage key yes TestMCPLoginStoresACredentialApartFromTheCLIs, TestMCPLoginStoresApartInThePlaintextFallbackToo
hey mcp falls back to the CLI's credential yes TestMCPCommandRefusesWithoutItsOwnCredential
hey mcp's manager honors HEY_TOKEN yes TestMCPManagerNeverUsesHEYToken
the granted scope is ignored (requested scope stored) yes TestMCPLoginHonorsTheScopeHEYGranted, TestMCPLoginRefusesAScopeItDidntAskFor
refresh names the CLI's client yes TestMCPRefreshNamesTheLocalClientAndKeepsTheScope
any answer to the elicitation counts as approval yes TestSendDraftWithoutApprovalSendsNothing
no fingerprint check on the second round yes TestSendDraftNamesTheChangeWhenOnlyTheContentMoved, TestSendDraftRefusesADraftChangedAfterApproval
no per-draft claim yes TestAFailedSendIsNeverRetriedAndStaysClaimed, TestAReplayedConfirmationSendsNothing, TestClaimsAndCapsSurviveARestart, TestSendDraftSendsADraftOnce, TestTwoConfirmationsOfOneDraftSendItOnce
a host without elicitation is asked anyway yes TestSendDraftCantSendFromAHostWithoutElicitation
reads never mark exposure yes TestAnExposedReadIsRefusedWhenItsMarkerCantBeWritten, TestAnUnscreenedReadRefusesSendsToNewRecipients
the claim doesn't re-read exposure yes TestTheClaimRereadsExposure
no new-address cap yes TestTheClaimEnforcesTheCaps
a failed fsync of the state file is ignored yes TestEitherFailedFsyncFailsTheClaim
a failed fsync of the directory is ignored yes TestEitherFailedFsyncFailsTheClaim
the claim doesn't refuse unseen content while exposed yes TestTheClaimRereadsExposure
exposure recorded only where hey_send is served yes TestAReadOnlyServerStillRecordsExposure
no check that the sign-in is unchanged yes TestASignInChangedUnderTheServerRefusesSendsAndExposingReads
unverifiable attachments sent yes TestAnAttachmentThatIsntAPlainBlobIsRefused
addresses truncated in the prompt yes TestTheConfirmationShowsAWholeAddressHoweverLong
the window keeps an address's first send time yes TestTheNewAddressWindowRollsWithTheLastSend
can_send ignores write yes TestMCPCanSendNeedsWriteAndSend
a delivery HEY kept as a draft reported sent yes TestADeliveryHEYKeptAsADraftIsntReportedSent
known by creation time yes TestADraftMadeBeforeTheSignInButSentAfterDoesntMakeARecipientKnown
styles checked by literal url( only yes TestAnEscapedOrFunctionalStyleCountsAsLoading
device link and code shown raw yes TestDeviceLoginSanitizesWhatItShows
attachment wrappers delivered as they came yes TestAnAttachmentIsDeliveredAsItsIdentityAlone, TestAnAttachmentReplacedWithTheSameNameAndSizeIsAChange
a pinned manager adopts another sign-in yes TestAPinnedManagerRefusesAnotherSignIn
conflicting client ids for one origin accepted yes TestConflictingSpellingsOfOneOriginAreRefused
the subject truncated in the prompt yes TestTheWholeSubjectIsShown
only listed invisible characters escaped yes TestEveryInvisibleCharacterIsEscaped
writes that answer content don't expose yes TestAWriteThatAnswersWithContentExposes
origin spelled as configured yes TestOriginHasOneSpelling
the send's client may refresh and resend yes TestMCPCommandNeverResendsADeliveryAfterA401
to someone new the draft goes as it is yes TestToSomeoneNewWhatGoesIsWhatWasShown
no check that the plain path held between rounds yes TestAPlainPathThatChangesBetweenRoundsRefusesTheSend
plain text keeps invisible characters yes TestToSomeoneNewWhatGoesIsWhatWasShown
no grant check right before the claim yes TestAGrantNarrowedDuringTheReviewDoesntClaimTheDraft
trailing whitespace trimmed before invisible characters go yes TestToSomeoneNewWhatGoesIsWhatWasShown
refusals echo the attachment's filename yes TestARefusalNamesNoDraftText
comments delivered yes TestACommentIsNeverSent
plain body lets spaces collapse yes TestThePlainBodyKeepsTheSpacingShown
an applied write reported as failed when its marker fails yes TestAWriteHEYAppliedIsAcknowledgedWhenItsMarkerFails

@jeremy

jeremy commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@codex review

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.

🔵 Needs a closer look

Production provisioning and live verification of security-sensitive authentication and attachment delivery remain pending, alongside an unresolved mutation-acknowledgement issue.

1 open issue
1 resolved since last review

Review effort: Balanced

Comment thread internal/mcpserver/dispatch.go

@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 Security Review · Automatically triggered

Here are some automated security review suggestions for this pull request.

Reviewed commit: 72f3cccc3a

ℹ️ 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.

Comment thread internal/mcpserver/sendprompt.go
…ts before the marker

- On the plain path, body and subject are NFKC-normalized after invisible
  characters go, so a no-break space and a decomposed letter go as what they
  look like. The subject sent is the one the confirmation showed.
- A write whose success is one exact status (rename_topic) is checked before
  the exposure marker, so a redirect is never reported as applied.
@jeremy

jeremy commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

Revert proof on 0c62b58. I took out each piece, ran the tests that hold it, and restored the file from HEAD. All 45 of the 45 pieces fail their tests when they are taken out, and make check is green with them in.

Piece taken out Fails Failing tests
hey mcp's credential shares the CLI's storage key yes TestMCPLoginStoresACredentialApartFromTheCLIs, TestMCPLoginStoresApartInThePlaintextFallbackToo
hey mcp falls back to the CLI's credential yes TestMCPCommandRefusesWithoutItsOwnCredential
hey mcp's manager honors HEY_TOKEN yes TestMCPManagerNeverUsesHEYToken
the granted scope is ignored (requested scope stored) yes TestMCPLoginHonorsTheScopeHEYGranted, TestMCPLoginRefusesAScopeItDidntAskFor
refresh names the CLI's client yes TestMCPRefreshNamesTheLocalClientAndKeepsTheScope
any answer to the elicitation counts as approval yes TestSendDraftWithoutApprovalSendsNothing
no fingerprint check on the second round yes TestSendDraftNamesTheChangeWhenOnlyTheContentMoved, TestSendDraftRefusesADraftChangedAfterApproval
no per-draft claim yes TestAFailedSendIsNeverRetriedAndStaysClaimed, TestAReplayedConfirmationSendsNothing, TestClaimsAndCapsSurviveARestart, TestSendDraftSendsADraftOnce, TestTwoConfirmationsOfOneDraftSendItOnce
a host without elicitation is asked anyway yes TestSendDraftCantSendFromAHostWithoutElicitation
reads never mark exposure yes TestAnExposedReadIsRefusedWhenItsMarkerCantBeWritten, TestAnUnscreenedReadRefusesSendsToNewRecipients
the claim doesn't re-read exposure yes TestTheClaimRereadsExposure
no new-address cap yes TestTheClaimEnforcesTheCaps
a failed fsync of the state file is ignored yes TestEitherFailedFsyncFailsTheClaim
a failed fsync of the directory is ignored yes TestEitherFailedFsyncFailsTheClaim
the claim doesn't refuse unseen content while exposed yes TestTheClaimRereadsExposure
exposure recorded only where hey_send is served yes TestAReadOnlyServerStillRecordsExposure
no check that the sign-in is unchanged yes TestASignInChangedUnderTheServerRefusesSendsAndExposingReads
unverifiable attachments sent yes TestAnAttachmentThatIsntAPlainBlobIsRefused
addresses truncated in the prompt yes TestTheConfirmationShowsAWholeAddressHoweverLong
the window keeps an address's first send time yes TestTheNewAddressWindowRollsWithTheLastSend
can_send ignores write yes TestMCPCanSendNeedsWriteAndSend
a delivery HEY kept as a draft reported sent yes TestADeliveryHEYKeptAsADraftIsntReportedSent
known by creation time yes TestADraftMadeBeforeTheSignInButSentAfterDoesntMakeARecipientKnown
styles checked by literal url( only yes TestAnEscapedOrFunctionalStyleCountsAsLoading
device link and code shown raw yes TestDeviceLoginSanitizesWhatItShows
attachment wrappers delivered as they came yes TestAnAttachmentIsDeliveredAsItsIdentityAlone, TestAnAttachmentReplacedWithTheSameNameAndSizeIsAChange
a pinned manager adopts another sign-in yes TestAPinnedManagerRefusesAnotherSignIn
conflicting client ids for one origin accepted yes TestConflictingSpellingsOfOneOriginAreRefused
the subject truncated in the prompt yes TestTheWholeSubjectIsShown
only listed invisible characters escaped yes TestEveryInvisibleCharacterIsEscaped
writes that answer content don't expose yes TestAWriteThatAnswersWithContentExposes
origin spelled as configured yes TestOriginHasOneSpelling
the send's client may refresh and resend yes TestMCPCommandNeverResendsADeliveryAfterA401
to someone new the draft goes as it is yes TestToSomeoneNewWhatGoesIsWhatWasShown
no check that the plain path held between rounds yes TestAPlainPathThatChangesBetweenRoundsRefusesTheSend
plain text keeps invisible characters yes TestToSomeoneNewWhatGoesIsWhatWasShown
no grant check right before the claim yes TestAGrantNarrowedDuringTheReviewDoesntClaimTheDraft
trailing whitespace trimmed before invisible characters go yes TestToSomeoneNewWhatGoesIsWhatWasShown
refusals echo the attachment's filename yes TestARefusalNamesNoDraftText
comments delivered yes TestACommentIsNeverSent
plain body lets spaces collapse yes TestThePlainBodyKeepsTheSpacingShown
an applied write reported as failed when its marker fails yes TestAWriteHEYAppliedIsAcknowledgedWhenItsMarkerFails
plain text not normalized yes TestThePlainPathNormalizesLookalikeText
the plain path sends the raw subject yes TestThePlainPathNormalizesLookalikeText
the acknowledgement checked after the marker yes TestARenameHEYRedirectedIsntReportedAppliedWhenTheMarkerFails

@github-actions github-actions Bot added the deps label Oct 24, 2026
@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. Can't wait for the next one!

Reviewed commit: 0c62b58346

ℹ️ 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".

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.

🔵 Needs a closer look

Security-sensitive authentication and delivery changes need Jeremy’s final review, production client provisioning, and the documented live HEY validation before the draft hold is lifted.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@jeremy

jeremy commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

Converged on 0c62b58, held as a draft for Jeremy. There is no auto-merge.

  • Reviews. Codex reports no major issues on 0c62b58. Copilot left no inline findings on it; its overview asks for Jeremy's review, production provisioning and live validation, which are the human steps below.
  • Threads. All 45 are resolved, and none needs a human decision beyond the ones listed under "Waiting on Jeremy".
  • CI is green on 0c62b58.
  • Revert proof: all 45 of the 45 pieces fail their tests when taken out (latest comment).

Findings per round: 11, 9, 7, 3, 2, 2, 1, 3, 4, 2, then 0.

Two of those rounds turned out to be the same weakness found again, so I replaced the mechanism instead of adding another check:

  • The attachment wrapper. It was checked as an opaque subtree, and four rounds each found a new way through. Attachments are now rebuilt from their identity, and nothing in the wrapper is sent.
  • The lossless-markup classifier. It couldn't be made injective. Sends to someone new, or while exposed, now go as exactly the plain text the confirmation showed.

Declined, with the reasoning in each thread:

Waiting on Jeremy:

  1. Provision HEY MCP (local) in production, then set config.DefaultMCPClientID.
  2. Validate against a live HEY (listed under "Not verified against a live HEY" in the PR body):
    • the scoped device flow;
    • the rebuilt attachment figure;
    • the known-recipient search;
    • send-unchecked consent.
  3. Decide on merging and the release note: existing hey mcp setups need hey auth login --mcp once.

This branch has not been deployed

No deployments
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