# Codex MCP keychain re-prompt - fix + QA

Spec: `kandan/server_v2/plans/2026-06-24-codex-mcp-keychain-reprompt-report.md`

## The problem (one paragraph)

Codex stores its MCP OAuth secret in a macOS login-keychain generic-password
item labeled `Codex MCP Credentials`. The item's access-control list is bound to
one exact codex binary (its CDHash). When Codex.app auto-updates (new CDHash),
or you move between codex identities (official OpenAI team `2DC432GLL2`, the
unsigned npm `codex` under `node`, or a Linzumi-re-signed codex), the CDHash no
longer matches the ACL and macOS re-prompts: "codex wants to use the Codex MCP
Credentials in your keychain". The proper upstream fixes (a Designated-
Requirement-bound ACL, or moving to the data-protection keychain) are OpenAI's.

## The no-upstream fix (lever 2)

Set the keychain item's PARTITION LIST to the code-signing team id(s) so any
same-team-signed codex build is granted silent access across updates, instead of
binding access to a single CDHash. This is what the
`linzumi fix-codex-keychain` subcommand does. The exact command it runs per
matching item is:

```
security set-generic-password-partition-list \
  -S "teamid:2DC432GLL2,teamid:2CWCP5BR45,apple:" \
  -s "Codex MCP Credentials" \
  -a <account> \
  -k <login-keychain-password>
```

- `2DC432GLL2` - OpenAI Codex code-signing team (the official Codex.app).
- `2CWCP5BR45` - Linzumi's macOS team
  (`kandan/kandan-electron-shell/scripts/common_macos_beta.sh` `DEFAULT_TEAM_ID`,
  overridable via `KANDAN_DEVELOPMENT_TEAM`; add more with `--team-id`).
- `apple:` - keeps Apple-provided tooling working.

The helper is macOS-only, scoped exclusively to the `Codex MCP Credentials`
service, idempotent, and consent-gated: it defaults to a dry run and only
mutates with `--apply`. The login-keychain password is never logged or
persisted (pass it via `LINZUMI_KEYCHAIN_PASSWORD` for automation, or let macOS
prompt).

### Using the helper

```
# Dry run - shows the item(s) and the partition list it would apply.
linzumi fix-codex-keychain

# Apply. macOS authorizes the ACL change with your login-keychain password.
linzumi fix-codex-keychain --apply

# Add an extra signing team (e.g. a custom re-signed codex).
linzumi fix-codex-keychain --apply --team-id ABCDE12345
```

## Automated synthetic repro (no GUI, no real item)

`scripts/qa/codex-keychain-partition-repro.sh` proves the mechanism the helper
relies on, without touching your real login keychain or the real
`Codex MCP Credentials` item, and without needing a real Codex.app update.

It creates a dedicated, unlocked test keychain with a known password, adds a
test item whose partition list does NOT include the target team (the would-
re-prompt precondition), runs the exact `set-generic-password-partition-list`
command the helper runs, then reads the partition list back with
`security dump-keychain -a` to prove the target team flipped from absent to
present. The test keychain is deleted on exit via a `trap` (even on failure).

Run it:

```
bash packages/linzumi-cli/scripts/qa/codex-keychain-partition-repro.sh
```

Expected output ends with:

```
== BEFORE: target team must be ABSENT from the partition list ==
partition list (before): apple:
ok: target team is absent before the fix (a same-team codex would re-prompt).
...
== AFTER: target team must now be PRESENT in the partition list ==
partition list (after):  teamid:2DC432GLL2, apple:
ok: target team is present after the fix (same-team codex builds read silently).
...
RESULT: PASS (partition list flipped from team-absent to team-present)
```

### What the synthetic repro does and does NOT prove

- PROVES (headless, deterministic): the fix command writes the intended
  partition list onto the item; the team id transitions from absent to present,
  observed verbatim via `security dump-keychain -a`. This is the load-bearing
  state change.
- Does NOT prove headlessly: that a same-team-signed codex binary then reads the
  secret with zero GUI prompt. Demonstrating the silent read requires a binary
  actually signed by the team in the partition; `/usr/bin/security` is not, and
  the authorization for a non-member reader is a GUI event that cannot be
  satisfied non-interactively (it returns errSec interaction-not-allowed in an
  automated shell). That payoff is confirmed by the on-device manual procedure
  below, which is itself inherently a GUI observation (the re-prompt is a dialog).

## On-device manual verification (real Codex.app item)

This is the human, on-device confirmation of the end-user payoff. Do it on a Mac
that actually uses Codex.app MCP credentials.

1. Observe the BEFORE re-prompt. Trigger codex MCP usage (or force a re-prompt by
   updating Codex.app, or temporarily switching codex identities). macOS shows:
   "codex wants to use the Codex MCP Credentials in your keychain". This dialog
   is the bug; note that it appears.
2. Apply the fix:
   ```
   linzumi fix-codex-keychain          # dry run: confirm the item + plan
   linzumi fix-codex-keychain --apply  # enter your login-keychain password once
   ```
3. Confirm AFTER. Relaunch codex / Codex.app (or re-trigger the same MCP usage,
   ideally after an app update so the CDHash differs). The credential is read
   with NO re-prompt. To double-check the state without a relaunch:
   ```
   security dump-keychain -a login.keychain-db \
     | grep -A2 'authorizations (1): partition_id'
   ```
   The `description:` line for the `Codex MCP Credentials` item now lists
   `teamid:2DC432GLL2` (and `teamid:2CWCP5BR45`, `apple:`).

Re-running `linzumi fix-codex-keychain --apply` is harmless (idempotent).
