pendingyou 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,80 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.0 (2026-10-04)
4
+
5
+ - Claude Code's permission prompts on a card (Darin, 2026-10-04: "Tell me: a card and a push, cleared once I answer in
6
+ the terminal"). A Claude Code agent kept asking to remove files and sat waiting until Darin happened to look. `init`
7
+ now adds Claude Code's PermissionRequest hook (https://code.claude.com/docs/en/hooks: it runs as Claude Code is about
8
+ to ask for permission to use a tool, with `tool_name` and `tool_input`), through the shim with `--app claude-code`.
9
+ It never decides: it prints nothing, so the prompt shows exactly as before, and it's done in about a tenth of a
10
+ second. It writes the prompt down (its card's redacted words, never the tool's input) in the session's own file,
11
+ `~/.config/pendingyou/permission-cards/<session>.json`, and starts that session's one worker, detached (`pendingyou
12
+ permission-card`). Once a prompt has waited 10 seconds (one answered sooner gets no card), the worker posts one card
13
+ through Pending You's MCP with Claude Code's own connection here (whoami, match_area, post_request): an action card
14
+ titled “Claude Code is waiting for your OK: rm -rf dist/” (at most 90 characters; “…waiting for your answer: …” for
15
+ a question it asked, “…on its plan” for a plan), the folder and the computer in its summary, one step, “Answer it in
16
+ Claude Code on Ann-MBP” (with the session's link while Remote Control is on), asked in both places (`askedFirst`
17
+ terminal, at the prompt's time) so a push comes only after the person's hand-off wait, not blocking, urgency now. It
18
+ goes in the area `match_area` picks for the folder and its git remote (a new one for the work when nothing fits, as
19
+ the guide says), under the name the session gave Pending You (read from its transcript), else “Claude Code”, with an
20
+ idempotency key per prompt. One card per session: a newer prompt changes its words (`update_request`, with a
21
+ changeNote), never a second card. It goes once the person answers: the call running (PostToolUse, or
22
+ PostToolUseFailure) withdraws it with `answeredHere`; a denial runs neither, so the person's next message (the
23
+ hand-off hook) or the end of the turn (the Stop hook) does; the session ending (SessionEnd) withdraws it without. A
24
+ card they pressed Done on is acknowledged instead. A subagent's prompt outlasts the person's message, and the end of a
25
+ turn while a subagent still runs in the background. No card where nobody sees the prompt (`claude -p` and the Agent
26
+ SDK, whose CLAUDE_CODE_ENTRYPOINT starts `sdk-`; a dontAsk or bypassPermissions session), for a sign-in that only
27
+ hears (`--oauth`), or on Windows.
28
+ - Nothing secret on those cards (`src/redact.ts`). A command is cut to 120 characters, with the values after anything
29
+ named like a secret blanked (`--token …`, `--password=…`, `-p …` but not `mkdir -p`, `mysql -p…`, `-u user:…`,
30
+ `Authorization: …`, `Cookie: …`, `Bearer …`, `FOO_TOKEN=…`, `"password": "…"`, `aws_secret_access_key …`), a URL's
31
+ user and password and every query and fragment value blanked, and anything that looks like a key or token (the
32
+ well-known prefixes, JWTs, private keys, long hex, long runs of letters and digits in both cases, a webhook's secret
33
+ in a URL's path). A command that can't be shown safely (more than one line, a heredoc, inline code such as `node -e`
34
+ or `bash -c`, `eval`, or one that takes or sets a secret, like `sudo -S`, `--password-stdin` or `gh secret set`)
35
+ shows as the tool and its first word: “Bash: node …”. A path is shown under `~`, its end kept when it's long; a
36
+ question or a search keeps its words with any key taken out. The git remote `match_area` gets has no user name or
37
+ password.
38
+ - `init --no-permission-cards` leaves them out (later runs keep them out; `--permission-cards` puts them back), and
39
+ `status` has a line: “Permission prompts: a card when Claude Code waits for your OK”, off as the person chose, or why
40
+ this Claude Code can't have them. They need Claude Code 2.1.119 or later (PermissionRequest came in 2.0.45, and
41
+ before 2.1.101 a hook event Claude Code didn't know made it ignore the whole settings file) and Claude Code signing
42
+ in through this computer's sign-in. The SessionEnd hook sets no timeout of its own, so Claude Code's 1.5 seconds for
43
+ SessionEnd hooks stays as it was. The shim skips Node for `permission-done` while no session has a prompt written
44
+ down, so a tool call costs a few milliseconds (20 ms measured, 8 of them the shell's own); the PermissionRequest hook
45
+ takes about 0.14 s, as long as the other hooks. `uninstall` takes the hooks and the sessions' files out.
46
+ - The hand-off hook names no permission prompt's card among the agent's open cards, and neither hook hands an agent a
47
+ Done the person pressed on one: those close by themselves.
48
+ - The Codex app for macOS ("ChatGPT.app", bundle id com.openai.codex; 26.928 carries codex 0.159.2), which shares
49
+ `~/.codex` with Codex CLI: its MCP servers (the headers helper too), `~/.agents/skills` and `~/.codex/hooks.json`
50
+ with the same trust. Where `codex` isn't on PATH, `init` and `status` find the app's own codex
51
+ (`Contents/Resources/codex-cli/bin/codex`, or the `CodexCLI.app` the app's own code looks for there, in
52
+ `/Applications` or `~/Applications`, as ChatGPT.app or Codex.app) and run it for the version check (0.152 or later,
53
+ for the headers helper), `codex mcp list` and `codex mcp logout`; init says it found "Codex 0.159.2 (the Codex
54
+ app’s)", and `status` names its path. The listener runs it for `codex queue`: the app's own server reads the queue
55
+ while the app is open, so a chat it has loaded is woken when you answer (0.149 or later; one it let go, idle 3 hours
56
+ or behind 10 newer idle chats, hears it when it's opened again), and `status`'s Wake line says so.
57
+ - Codex's last lines name both ways to trust its hooks (“In Codex CLI choose “Trust all and continue” (or trust them in
58
+ /hooks); in the Codex app click Review hooks › Allow all (or Settings › Hooks › Trust all)”; the app alone: open it
59
+ and click Review hooks › Allow all), and say not to press Save on pendingyou in the Codex app's MCP settings: that
60
+ writes the table again without `http_headers_helper` (and `default_tools_approval_mode`), so Codex no longer signs in
61
+ through this computer's sign-in. `status` says so when the table has lost the helper init gave it (“has lost its
62
+ http_headers_helper … (saving it in the Codex app’s MCP settings does that) … run npx pendingyou init --app codex
63
+ again”), and init puts it back (“Put this computer’s sign-in back on Codex’s pendingyou MCP server”). Its Hooks line
64
+ names both ways to trust them.
65
+ - The setup line every app's session start gives, and the Claude Code wake mod's setup turn, say exactly the three
66
+ calls: “Pending You: Codex on Ann-MBP is connected, but its setup isn’t finished. Three calls, nothing to install or
67
+ approve: 1. whoami with your name. If it says app.setUp is true, another session did this: stop. 2. report_setup with
68
+ source codex, your name, skillSaved true, hears instant, and skillVersion set to whoami’s skill.latest. Its answer
69
+ has a testAreaId. 3. post_request a short test question in that testAreaId, tell your person it’s on its way, and end
70
+ your turn: you’re woken when they answer, so don’t ask them to type anything.” Pi's first setup on Darin's devbox
71
+ spent about 20,000 tokens on the words before: “whoami with your name and guide true” answered with the whole guide
72
+ (87,000 characters), which Pi cut into a file the model then parsed, and “your test question in its testAreaId” sent
73
+ it looking for testAreaId in whoami's answer, where it isn't (report_setup's has it). A setup that has reported gets
74
+ the same calls without whoami's way out (its whoami says app.setUp is true); a test card answered says who picks it
75
+ up, with the call's exact arguments. Pi's extension knows the new lines (and 0.12.0's) to start the setup turn; the
76
+ wake mod names the connection from whoami's answer.
77
+
3
78
  ## 0.12.0 (2026-10-04)
4
79
 
5
80
  - The stdio bridge, `pendingyou mcp --app <app>`: a local MCP server for an agent that can't give its MCP client a
package/README.md CHANGED
@@ -24,7 +24,8 @@ Run it once on each computer, in a terminal. It finds the agents on the computer
24
24
  Pi reads Codex's, else `~/.pi/agent/skills/pendingyou/SKILL.md`),
25
25
  - adds the hooks that hand it your answers: a new session picks up answers that arrived while nothing was running,
26
26
  your next message brings a waiting answer with it, and a check when it finishes a turn catches what it left you only
27
- in chat ([below](#nothing-left-only-in-chat)),
27
+ in chat ([below](#nothing-left-only-in-chat)); and, for Claude Code, a card when it waits for your OK to run something
28
+ ([below](#when-claude-code-waits-for-your-ok)),
28
29
  - and wakes it when you answer: Claude Code 2.1.287 or later through the wake mod, Codex through `codex queue`, OpenCode
29
30
  through its plugin and Pi through its extension (which also do the hooks' work there).
30
31
 
@@ -38,8 +39,9 @@ Finish setting up Pending You: it’s connected here already.
38
39
  ```
39
40
 
40
41
  The first session finishes setting up on its own: it checks in with Pending You, reports what's set up and sends you a
41
- test card. Claude Code 2.1.287 or later starts that turn by itself; otherwise (and in Codex, after you choose “Trust all
42
- and continue” for its hooks) the session start adds one line saying what's left, and you say the sentence.
42
+ test card. Claude Code 2.1.287 or later starts that turn by itself; otherwise (and in Codex, after you trust its hooks:
43
+ “Trust all and continue” in Codex CLI, Review hooks › Allow all in the Codex app) the session start adds one line with
44
+ the three calls that are left, and you say the sentence.
43
45
  `npx pendingyou status` says what's set up for each agent, what's missing with the command that fixes it, and how setup
44
46
  stands. `npx pendingyou uninstall` removes exactly what `init` added.
45
47
 
@@ -92,14 +94,24 @@ default_tools_approval_mode = "approve"
92
94
  ```
93
95
 
94
96
  and four hooks in `~/.codex/hooks.json`: a session start, each message, Pending You's card tools (so the Stop check
95
- knows a turn posted, and so the thread is woken), and the Stop check. **Codex runs a hook only once you trust it**: start
96
- `codex` and choose “Trust all and continue” (or trust them in `/hooks`). `status` says which are trusted. A
97
- `pendingyou` server or hooks Codex's `/import` copied from Claude Code (without its sign-in) are replaced.
97
+ knows a turn posted, and so the thread is woken), and the Stop check. **Codex runs a hook only once you trust it**: in
98
+ Codex CLI, start `codex` and choose “Trust all and continue” (or trust them in `/hooks`); in the Codex app, click
99
+ Review hooks › Allow all (or Settings › Hooks › Trust all). `status` says which are trusted. A `pendingyou` server or
100
+ hooks Codex's `/import` copied from Claude Code (without its sign-in) are replaced.
101
+
102
+ **The Codex app** (on a Mac, in the ChatGPT app) shares all of this with Codex CLI: the same `~/.codex`, server, hooks
103
+ and trust. With no `codex` on PATH, `init` uses the app's own (inside `/Applications/ChatGPT.app` or
104
+ `~/Applications`), for the version and everything else it runs. Don't press Save on pendingyou in the app's MCP
105
+ settings: that writes the table again without `http_headers_helper` (and `default_tools_approval_mode`), so Codex stops
106
+ signing in through this computer's sign-in. `status` says so when it happens; `npx -y pendingyou@latest init` puts it
107
+ back.
98
108
 
99
109
  When you answer a card a Codex thread posted, a small listener `init`'s hooks start for that thread puts a message into
100
110
  it with `codex queue`: “Pending You: your person answered “…” (req_…). Call get_request …”, which reads your answer (the
101
111
  answer itself never goes in a command's arguments, where other programs could see it). Codex starts a turn at
102
- once when the thread is open and idle, after the current one when it's working, or when you next open it.
112
+ once when the thread is open and idle, after the current one when it's working, or when you next open it. In the Codex
113
+ app, a chat it has open is woken while the app runs (one it let go, idle 3 hours or behind 10 newer idle chats, when
114
+ you open it again).
103
115
 
104
116
  ## OpenCode
105
117
 
@@ -199,6 +211,28 @@ and Pi's extension (0.12.0) do the same in theirs.
199
211
  **On 0.2.0 with another origin?** Its hooks blocked every message (`There's no "--origin" command`). Run
200
212
  `npx pendingyou@latest init --origin <url>` again to repair them; see [CHANGELOG.md](CHANGELOG.md).
201
213
 
214
+ ## When Claude Code waits for your OK
215
+
216
+ Claude Code stops to ask before it runs something it isn't allowed to run by itself (`rm -rf dist/`, a push, a file
217
+ outside the folder), and waits, however long you're away. Since 0.13.0, when you don't answer within 10 seconds, a card
218
+ says so: “Claude Code is waiting for your OK: rm -rf dist/”, with the folder and the computer, and one step: answer it
219
+ in Claude Code there (with the session's link while Remote Control is on, where you can answer it from your phone).
220
+ It's asked in both places, so you get a push only once it has waited your hand-off time (3 minutes unless you changed
221
+ it). It goes away by itself once you answer in Claude Code: allowed, denied, or the session closed. One card per
222
+ session: a newer prompt changes it.
223
+
224
+ The hook (Claude Code's PermissionRequest) never answers for you: it prints nothing, so the prompt shows exactly as
225
+ before, and it's done in about a tenth of a second. A small process in the background posts, changes and withdraws the
226
+ card through Pending You, with Claude Code's own connection here. Nothing secret goes on the card: values after
227
+ anything named like a key, token or password, a URL's user and password and query values, and anything that looks like
228
+ a key are blanked (`curl -H "Authorization: …" https://api.example.com/items?token=…`), and a command that can't be
229
+ shown safely (several lines, inline code like `node -e`, one that takes a secret) shows only as “Bash: node …”. No card
230
+ for `claude -p` or the Agent SDK, where nobody sees a prompt.
231
+
232
+ It needs Claude Code 2.1.119 or later, signing in through this computer's sign-in (not `--oauth`), and isn't on
233
+ Windows yet. Don't want it? `npx pendingyou init --no-permission-cards` (later runs keep it off; `--permission-cards`
234
+ turns it back on). `npx pendingyou status` says which.
235
+
202
236
  ## How Claude Code hears your answer
203
237
 
204
238
  **Claude Code 2.1.287 or later: the wake mod.** There's nothing to run. In each session the mod learns the cards
@@ -284,7 +318,7 @@ more, then skipped. `--once` stops after the first.
284
318
 
285
319
  | Command | What it does |
286
320
  | --- | --- |
287
- | `init` | Set up every agent on this computer (`--app`, `--oauth`, `--no-login`, `--no-browser`, `--device`, `--browser`, `--yes`, `--name`) |
321
+ | `init` | Set up every agent on this computer (`--app`, `--oauth`, `--no-login`, `--no-browser`, `--device`, `--browser`, `--yes`, `--name`, `--no-permission-cards`) |
288
322
  | `hold <requestId>` | Wait for the answer, print it, exit (`--timeout 4h`); the wake mod answers it itself |
289
323
  | `status` | What's set up for each agent; exits 0 when each hears answers right away (`--app`) |
290
324
  | `login` / `logout` | Sign an agent in again, or out (`--app`, `--device`, `--browser`, `--force`, `--name`, `--all`) |
@@ -311,12 +345,17 @@ Pending You (Claude Code says which server it's for; Codex's is the one `init` w
311
345
  sign-in each agent's connection used, so signing in again keeps it.
312
346
 
313
347
  `state.json` beside them remembers which answers were already handed over (ids only) and how many session starts
314
- reminded an unfinished setup, and when; `claude-code.json`, `codex.json`, `opencode.json` and `pi.json` what `init`
348
+ reminded an unfinished setup, and when; `permission-cards/` each Claude Code session's prompts still waiting on your OK
349
+ and its card (ids, hashes and the card's redacted words: never what the tool was given), gone once nothing waits;
350
+ `claude-code.json`, `codex.json`, `opencode.json` and `pi.json` what `init`
315
351
  added; `codex-threads.json`, `opencode-threads.json` and `pi-threads.json` which cards each Codex thread or OpenCode or
316
352
  Pi session waits on (ids, names and titles: never an answer); `pi/` Pi's extension; and `cli/` the hooks' copy of the
317
353
  command line. `init`, `status` and the hooks read `~/.claude.json` (and a folder's `.mcp.json`, and
318
354
  `~/.claude/settings.json`'s enabled plugins), `~/.codex/config.toml`, OpenCode's config and Pi's `mcp.json` only to see
319
- whether the agent has a `pendingyou` MCP server and where it points. Nothing is sent anywhere but Pending You. OpenCode's plugin reads its session's messages only when it goes idle, for the check that
355
+ whether the agent has a `pendingyou` MCP server and where it points. For a permission prompt's card, the worker reads
356
+ the end of the Claude Code session's transcript only for the name the session gave Pending You, and `git remote
357
+ get-url origin` in its folder (sent without any user name or password in it). Nothing is sent anywhere but Pending
358
+ You. OpenCode's plugin reads its session's messages only when it goes idle, for the check that
320
359
  nothing was left in chat, and hands the hooks' script just that turn's words and tool names.
321
360
 
322
361
  The wake mod runs inside Claude Code and reaches Pending You only through the session's own connection, with
@@ -11,7 +11,7 @@
11
11
  // to ask it's kept, and init says how to switch. Kept, Claude Code signs in by itself and the hooks get a sign-in
12
12
  // that only hears; kept beside the plugin, no skill is saved either: the plugin carries it.
13
13
  // - one at another Pending You: replaced once the person says so (or `--yes`), or else Claude Code is left alone.
14
- import { addCommand, claudeState, helperServer, installClaude, mcpFromClaude, mcpFromFile, PLUGIN_ORIGINS, pluginFor, readManifest, removeCommand, sameUrl, uninstallClaude, } from "../claude.js";
14
+ import { addCommand, claudeState, helperServer, installClaude, mcpFromClaude, mcpFromFile, PLUGIN_ORIGINS, permissionBlocker, pluginFor, readManifest, removeCommand, sameUrl, uninstallClaude, } from "../claude.js";
15
15
  import { connectionMachine, readCredential } from "../credentials.js";
16
16
  import { differs, serverForFolder, siteOf } from "../environment.js";
17
17
  import { originArgs } from "../hooks.js";
@@ -185,6 +185,7 @@ export const claudeCode = {
185
185
  hookLine: ictx.hookLine,
186
186
  report: ictx.report,
187
187
  progress: ictx.progress,
188
+ ...(ctx.permissionCards === undefined ? {} : { permissionCards: ctx.permissionCards }),
188
189
  }),
189
190
  next: (ictx) => {
190
191
  if (mode === 'oauth') {
@@ -223,6 +224,23 @@ export const claudeCode = {
223
224
  return claudeStatus(io, ctx);
224
225
  },
225
226
  };
227
+ /**
228
+ * status's line for the permission-prompt hooks (0.13.0): on; off, as the person chose; or why Claude Code here can't
229
+ * have them. Never a reason status isn't Ready.
230
+ */
231
+ function permissionLine(io, claude, manifest, helper, flag) {
232
+ const label = 'Permission prompts:';
233
+ if (claude.permission)
234
+ return ` ok ${label} a card when Claude Code waits for your OK`;
235
+ if (manifest?.permissionCards === false)
236
+ return ` ${label} off, as you chose; for a card when Claude Code waits for your OK, run npx pendingyou init --permission-cards${flag}`;
237
+ const blocker = claude.claude
238
+ ? permissionBlocker(io, claude.claude, helper ? 'helper' : 'oauth')
239
+ : null;
240
+ return blocker
241
+ ? ` ${label} no card when Claude Code waits for your OK: ${blocker}`
242
+ : ` missing ${label} no card when Claude Code waits for your OK; run npx pendingyou@latest init${flag}`;
243
+ }
226
244
  /** Claude Code's section of `pendingyou status`: what's set up and what's missing, each with the command that fixes it. */
227
245
  async function claudeStatus(io, ctx) {
228
246
  const { origin, signIn } = ctx;
@@ -338,7 +356,7 @@ async function claudeStatus(io, ctx) {
338
356
  ? [
339
357
  ` ok Plugin: ${plugin} is enabled${pluginServer ? '' : mcp !== false && mcp !== 'unknown' ? '; Claude Code uses the server added by hand instead of its own' : ''}`,
340
358
  ]
341
- : []), ` ${mark(skillOk)} Skill: ${skillText}`, ` ${mark(claude.hooks && hooksRun)} Hooks: ${claude.hooks ? `session-start pickup and next-message hand-off; ${formText}` : `not installed; run npx pendingyou init${flag}`}`, ` ${mark(claude.stopcheck)} Stop check: ${claude.stopcheck ? 'asks Claude Code to post what it leaves you in chat' : `not installed; run npx pendingyou init${flag}`}`, ` ${mark(modOk)} Wake mod: ${modText}`);
359
+ : []), ` ${mark(skillOk)} Skill: ${skillText}`, ` ${mark(claude.hooks && hooksRun)} Hooks: ${claude.hooks ? `session-start pickup and next-message hand-off; ${formText}` : `not installed; run npx pendingyou init${flag}`}`, ` ${mark(claude.stopcheck)} Stop check: ${claude.stopcheck ? 'asks Claude Code to post what it leaves you in chat' : `not installed; run npx pendingyou init${flag}`}`, ` ${mark(modOk)} Wake mod: ${modText}`, permissionLine(io, claude, manifest, helper, flag));
342
360
  // How setup stands on Pending You (from servers that say): never a reason it isn't Ready.
343
361
  const setup = setupStatus(NAME, signIn.state === 'ok' ? setupOf(signIn.for) : null);
344
362
  if (setup)
@@ -0,0 +1,44 @@
1
+ // The Codex app for macOS (0.13.0; research 2026-10-04): "ChatGPT.app" (bundle id com.openai.codex; 26.928 carries codex
2
+ // 0.159.2) has its own codex command line inside it, and shares ~/.codex with Codex CLI: its MCP servers (the headers
3
+ // helper too), ~/.agents/skills, ~/.codex/hooks.json and the hooks' trust. Its hooks run with a PATH that ends in its own
4
+ // codex, and its private Codex server reads ~/.codex/queue_1.sqlite every 10 seconds, so `codex queue --thread` wakes a
5
+ // chat the app has loaded while it's open (an idle chat is let go after 3 hours, or behind 10 newer idle ones; then the
6
+ // message waits until the chat is opened again). Where `codex` isn't on PATH, init, status and the listener
7
+ // (codex-wake.ts) run the app's own instead: these paths, in /Applications or ~/Applications, under either name.
8
+ import { stat } from 'node:fs/promises';
9
+ import { join } from 'node:path';
10
+ /** The app's names: the ChatGPT desktop app that carries Codex, and Codex's own, should it ship as that. */
11
+ const APP_NAMES = ['ChatGPT.app', 'Codex.app'];
12
+ /**
13
+ * Where a Codex app keeps its own codex: `Contents/Resources/codex-cli/bin/codex`, and the CodexCLI.app inside it that
14
+ * the app's own code looks for (`codex-cli/CodexCLI.app/Contents/MacOS/codex`), in /Applications, then ~/Applications.
15
+ * `system` is /Applications unless PENDINGYOU_APPLICATIONS_DIR says otherwise (tests: they never look in the real one).
16
+ */
17
+ export function appCodexPaths(home, system = '/Applications') {
18
+ return [system, join(home, 'Applications')].flatMap((root) => APP_NAMES.flatMap((name) => {
19
+ const cli = join(root, name, 'Contents', 'Resources', 'codex-cli');
20
+ return [join(cli, 'bin', 'codex'), join(cli, 'CodexCLI.app', 'Contents', 'MacOS', 'codex')];
21
+ }));
22
+ }
23
+ /** `codex --version`'s version ("codex-cli 0.159.2"), or "installed" when it can't be read. */
24
+ const versionOf = (stdout) => /(\d+\.\d+\.\d+)/.exec(stdout)?.[1] ?? 'installed';
25
+ const executable = (path) => stat(path).then((info) => info.isFile() && (info.mode & 0o111) !== 0, () => false);
26
+ /**
27
+ * The codex to run: `codex` on PATH; else, on a Mac, the Codex app's own (the first of `paths` there that answers
28
+ * `--version`); null when there's neither.
29
+ */
30
+ export async function findCodex(io, paths = appCodexPaths(io.home, io.env.PENDINGYOU_APPLICATIONS_DIR || undefined)) {
31
+ const onPath = await io.run('codex', ['--version'], 20_000);
32
+ if (onPath.code === 0)
33
+ return { command: 'codex', version: versionOf(onPath.stdout), app: false };
34
+ if (io.platform !== 'darwin')
35
+ return null;
36
+ for (const path of paths) {
37
+ if (!(await executable(path)))
38
+ continue;
39
+ const result = await io.run(path, ['--version'], 20_000);
40
+ if (result.code === 0)
41
+ return { command: path, version: versionOf(result.stdout), app: true };
42
+ }
43
+ return null;
44
+ }
@@ -31,6 +31,7 @@ import { readCredential } from "../credentials.js";
31
31
  import { configDir, readJson, withLock, writeWhole } from "../files.js";
32
32
  import { clip, isClosed } from "../format.js";
33
33
  import { markHanded, momentOf, wasHanded } from "../state.js";
34
+ import { findCodex } from "./codex-app.js";
34
35
  /** How long a turn's record, and a thread untouched, are kept. */
35
36
  const KEEP_TURN_MS = 24 * 60 * 60_000;
36
37
  const KEEP_THREAD_MS = 7 * 24 * 60 * 60_000;
@@ -320,8 +321,18 @@ async function renew(io, app, thread, id) {
320
321
  await utimes(path, now, now).catch(() => { });
321
322
  return true;
322
323
  }
323
- /** Codex: `codex queue`, which starts the turn itself. */
324
- const byCodexQueue = (io, thread) => async (message) => (await io.run('codex', ['queue', '--thread', thread, '--message', message], 30_000)).code === 0;
324
+ /**
325
+ * Codex: `codex queue`, which starts the turn itself: the codex on PATH, or the Codex app's own (0.13.0, codex-app.ts),
326
+ * found once, when the listener first wakes the thread.
327
+ */
328
+ function byCodexQueue(io, thread) {
329
+ let program = null;
330
+ return async (message) => {
331
+ program ??= findCodex(io).then((found) => found?.command ?? 'codex');
332
+ const queued = await io.run(await program, ['queue', '--thread', thread, '--message', message], 30_000);
333
+ return queued.code === 0;
334
+ };
335
+ }
325
336
  /**
326
337
  * OpenCode and Pi: the plugin (Pi: the extension) that started this listener reads the wake on stdout and starts the
327
338
  * turn, then says on stdin whether it did. stdin closing means the plugin (the app) is gone: `gone` stops the listener.
@@ -20,17 +20,22 @@
20
20
  // which it does by itself from 0.157): codex-wake.ts.
21
21
  // - Codex's /import can copy Claude Code's Pending You server (without its headersHelper, so it never signs in) and its
22
22
  // hooks (with Claude Code's app): init replaces both.
23
+ // - The Codex app for macOS (0.13.0, codex-app.ts) shares all of ~/.codex with Codex CLI, and carries its own codex:
24
+ // with none on PATH, init, status and the listener run the app's. Its trust is "Review hooks › Allow all", and Save on
25
+ // pendingyou in its MCP settings writes the table again without http_headers_helper (and the approval mode): init's
26
+ // last lines say both, status names the dropped helper, and running init again puts it back.
23
27
  import { createHash } from 'node:crypto';
24
28
  import { realpath, rm } from 'node:fs/promises';
25
29
  import { join } from 'node:path';
26
30
  import { removeSkill, saveSkill } from "../claude.js";
27
31
  import { readCredential } from "../credentials.js";
28
32
  import { configDir, readJson, readText, writeWhole } from "../files.js";
29
- import { isOurHook, mergeHooks, originArgs, removeHooks, } from "../hooks.js";
33
+ import { isOurHook, mergeHooks, originArgs, removeHooks, shWord, } from "../hooks.js";
30
34
  import { atLeast } from "../mod.js";
31
35
  import { checkHelper, foundBy, headlessReason, helperCommand, helperPath, isOurHelper, } from "../remote.js";
32
36
  import { connectionTitle, FINISH_SAY, setupOf, setupStatus } from "../setup.js";
33
37
  import { basicString, definedInline, headerOf, renameTable, tableSpans, tableValues, withTable, } from "../toml.js";
38
+ import { findCodex } from "./codex-app.js";
34
39
  import { APP_NAMES } from "./ids.js";
35
40
  const NAME = APP_NAMES.codex;
36
41
  const SERVER = ['mcp_servers', 'pendingyou'];
@@ -38,6 +43,18 @@ const SERVER = ['mcp_servers', 'pendingyou'];
38
43
  export const CODEX_MIN = '0.152.0';
39
44
  /** From here interactive Codex starts its shared background server by itself, so `codex queue` wakes an idle thread. */
40
45
  export const CODEX_WAKE = '0.157.0';
46
+ /**
47
+ * From here `codex queue` is there (0.149), which is all the Codex app needs: its own server reads the queue while the
48
+ * app is open (codex-app.ts).
49
+ */
50
+ export const CODEX_APP_WAKE = '0.149.0';
51
+ /** Where init found Codex when it isn't on PATH. */
52
+ const FROM_APP = 'the Codex app’s';
53
+ /** How the person trusts Codex's hooks, in Codex CLI and in the Codex app (0.13.0). */
54
+ const TRUST_CLI = 'in Codex CLI choose “Trust all and continue” (or trust them in /hooks)';
55
+ const TRUST_APP = 'in the Codex app click Review hooks › Allow all (or Settings › Hooks › Trust all)';
56
+ /** What undoes init in the Codex app: its MCP settings' Save writes the table again without the helper. */
57
+ const DONT_SAVE = 'In the Codex app, don’t press Save on pendingyou in Settings › MCP: that writes it again without this computer’s sign-in (if you did, run init again to put it back).';
41
58
  /** The Pending You tools whose calls the PostToolUse hook reads (codex-wake.ts). */
42
59
  const CARD_TOOLS = '^mcp__pendingyou__(post_request|update_request|reply_in_thread|cancel_request)$';
43
60
  /** Codex's hooks, by event: the command each runs, and its matcher. */
@@ -210,8 +227,8 @@ async function hookTrust(io) {
210
227
  });
211
228
  }
212
229
  /** Whether `codex mcp list` says Codex holds an OAuth sign-in for its pendingyou server; null when it can't say. */
213
- async function signedInByItself(io) {
214
- const result = await io.run('codex', ['mcp', 'list', '--json'], 30_000);
230
+ async function signedInByItself(io, program) {
231
+ const result = await io.run(program, ['mcp', 'list', '--json'], 30_000);
215
232
  if (result.code !== 0)
216
233
  return null;
217
234
  try {
@@ -279,10 +296,14 @@ export const codex = {
279
296
  name: NAME,
280
297
  minVersion: CODEX_MIN,
281
298
  async detect(io) {
282
- const result = await io.run('codex', ['--version'], 20_000);
283
- if (result.code !== 0)
299
+ // `codex` on PATH, else the Codex app's own (codex-app.ts): init, status and the listener run that one.
300
+ const found = await findCodex(io);
301
+ if (!found)
284
302
  return null;
285
- return { version: /(\d+\.\d+\.\d+)/.exec(result.stdout)?.[1] ?? 'installed' };
303
+ return {
304
+ version: found.version,
305
+ ...(found.app ? { command: found.command, from: FROM_APP } : {}),
306
+ };
286
307
  },
287
308
  async installed(io) {
288
309
  return (await readCodexManifest(io)) !== null;
@@ -311,6 +332,8 @@ export const codex = {
311
332
  text: 'Codex on Windows isn’t set up by init yet: paste Pending You’s setup message for Codex into it instead.',
312
333
  });
313
334
  const helperMode = !ctx.oauth;
335
+ // The codex init runs: the one on PATH, or the Codex app's own.
336
+ const program = detected.command ?? 'codex';
314
337
  const server = await codexServer(io).catch(() => null);
315
338
  if (server?.inline)
316
339
  return skip({
@@ -351,7 +374,7 @@ export const codex = {
351
374
  text: `Kept Codex’s pendingyou MCP server: it signs in through a headers helper of its own (${server.helper}).`,
352
375
  };
353
376
  }
354
- else if ((await signedInByItself(io)) !== true) {
377
+ else if ((await signedInByItself(io, program)) !== true) {
355
378
  // Never signed in: copied from Claude Code by /import (which leaves its helper behind), or left half-made.
356
379
  action = 'write';
357
380
  }
@@ -397,7 +420,7 @@ export const codex = {
397
420
  });
398
421
  else {
399
422
  if (mode === 'helper' && server)
400
- await io.run('codex', ['mcp', 'logout', 'pendingyou'], 60_000);
423
+ await io.run(program, ['mcp', 'logout', 'pendingyou'], 60_000);
401
424
  const doc = (await readText(config).catch(() => null)) ?? '';
402
425
  const next = withTable(doc, SERVER, serverTable(doc, url, mode === 'helper' ? ictx.helper : null));
403
426
  if (await writeConfig(config, next)) {
@@ -408,11 +431,13 @@ export const codex = {
408
431
  ? `Added the pendingyou MCP server (${url}) to ${config}${mode === 'helper' ? ', signed in through this computer’s sign-in' : ''}; its tools run without asking.`
409
432
  : server.url && !sameUrl(server.url, url)
410
433
  ? `Replaced Codex’s pendingyou MCP server (was ${server.url}) with ${url}.`
411
- : mode === 'helper'
412
- ? `Switched Codex’s pendingyou MCP server (${url}) to this computer’s sign-in: one sign-in for it and its hooks.`
413
- : server.helper
414
- ? `Switched Codex’s pendingyou MCP server (${url}) to Codex’s own sign-in: run codex mcp login pendingyou.`
415
- : `Codex’s pendingyou MCP server (${url}) signs in by itself, and its tools now run without asking.`,
434
+ : mode === 'helper' && before?.helper && !server.helper
435
+ ? `Put this computer’s sign-in back on Codex’s pendingyou MCP server (${url}): it had lost its http_headers_helper, which saving it in the Codex app’s MCP settings does.`
436
+ : mode === 'helper'
437
+ ? `Switched Codex’s pendingyou MCP server (${url}) to this computer’s sign-in: one sign-in for it and its hooks.`
438
+ : server.helper
439
+ ? `Switched Codex’s pendingyou MCP server (${url}) to Codex’s own sign-in: run codex mcp login pendingyou.`
440
+ : `Codex’s pendingyou MCP server (${url}) signs in by itself, and its tools now run without asking.`,
416
441
  });
417
442
  }
418
443
  else
@@ -450,10 +475,12 @@ export const codex = {
450
475
  });
451
476
  }
452
477
  }
453
- if (!atLeast(version, CODEX_WAKE))
478
+ if (!atLeast(version, detected.command ? CODEX_APP_WAKE : CODEX_WAKE))
454
479
  report({
455
480
  ok: true,
456
- text: `Codex ${version} wakes an idle thread with your answer only while its background server runs, which Codex starts by itself from ${CODEX_WAKE}. Until you update, an answer reaches a thread when you next write in it.`,
481
+ text: detected.command
482
+ ? `The Codex app’s codex ${version} has no codex queue, so an answer reaches a chat only when you next write in it. Update the Codex app, then run init again.`
483
+ : `Codex ${version} wakes an idle thread with your answer only while its background server runs, which Codex starts by itself from ${CODEX_WAKE}. Until you update, an answer reaches a thread when you next write in it.`,
457
484
  });
458
485
  const manifest = {
459
486
  version: 1,
@@ -472,15 +499,14 @@ export const codex = {
472
499
  },
473
500
  next: () => {
474
501
  const login = mode === 'oauth' && action !== 'keep';
475
- const start = `start codex here, or in the folder you work in, and choose “Trust all and continue” when it asks about hooks (or trust Pending You’s in /hooks)`;
502
+ const first = login ? `run ${shWord(program)} mcp login pendingyou, then ` : '';
503
+ // Codex CLI and the Codex app share ~/.codex: name both ways to trust the hooks (0.13.0).
504
+ const start = detected.command
505
+ ? `open the Codex app, start a chat in the folder you work in, and trust Pending You’s hooks: click Review hooks › Allow all (or Settings › Hooks › Trust all)`
506
+ : `start codex here, or in the folder you work in, and trust Pending You’s hooks: ${TRUST_CLI}; ${TRUST_APP}`;
476
507
  return {
477
- lines: [
478
- `Next: ${login ? 'run codex mcp login pendingyou, then ' : ''}${start}.`,
479
- 'Then say this to it:',
480
- '',
481
- FINISH_SAY,
482
- ],
483
- together: `- Codex: ${login ? 'run codex mcp login pendingyou, then ' : ''}${start}, then say the line below to it.`,
508
+ lines: [`Next: ${first}${start}.`, DONT_SAVE, 'Then say this to it:', '', FINISH_SAY],
509
+ together: `- Codex: ${first}${start}, then say the line below to it. ${DONT_SAVE}`,
484
510
  say: true,
485
511
  };
486
512
  },
@@ -560,6 +586,8 @@ export const codex = {
560
586
  ? `not signed in; run ${fix}`
561
587
  : 'Pending You couldn’t be reached';
562
588
  const mcpOk = Boolean(server?.url && sameUrl(server.url, url) && (!helper || helped));
589
+ // init gave it the helper, and it's gone: saving it in the Codex app's MCP settings writes the table again without.
590
+ const dropped = Boolean(server?.url && sameUrl(server.url, url) && !server.helper && manifest?.helper);
563
591
  const mcpText = !server
564
592
  ? `not added; run npx pendingyou init --app codex${flag}`
565
593
  : server.inline
@@ -568,9 +596,11 @@ export const codex = {
568
596
  ? `points at ${server.url}, not ${url}; run npx pendingyou init --app codex${flag} to replace it`
569
597
  : helped
570
598
  ? `pendingyou (${server.url}), signed in through ${helperPath(io)} --app codex${server.approval === 'approve' ? '; its tools run without asking' : ''}`
571
- : helper
572
- ? `pendingyou (${server.url}) signs in by itself; run npx pendingyou init --app codex${flag} to use this computer’s sign-in`
573
- : `pendingyou (${server.url}), signed in by Codex itself`;
599
+ : dropped
600
+ ? `pendingyou (${server.url}) has lost its http_headers_helper${server.approval === 'approve' ? '' : ' and default_tools_approval_mode'} (saving it in the Codex app’s MCP settings does that), so Codex can’t sign in through this computer’s sign-in; run npx pendingyou init --app codex${flag} again`
601
+ : helper
602
+ ? `pendingyou (${server.url}) signs in by itself; run npx pendingyou init --app codex${flag} to use this computer’s sign-in`
603
+ : `pendingyou (${server.url}), signed in by Codex itself`;
574
604
  const check = helper ? await checkHelper(io, origin, 'codex') : null;
575
605
  const words = {
576
606
  trusted: 'trusted',
@@ -582,12 +612,14 @@ export const codex = {
582
612
  const hooksOk = trust.every((hook) => hook.trust === 'trusted');
583
613
  const hooksText = trust.some((hook) => hook.trust === 'missing')
584
614
  ? `not installed; run npx pendingyou init --app codex${flag}`
585
- : `${trust.map((hook) => `${hook.event} ${words[hook.trust]}`).join(', ')}${hooksOk ? '' : '. Start codex and choose “Trust all and continue”, or trust them in /hooks: Codex skips a hook you haven’t trusted'}`;
586
- const wakes = detected ? atLeast(detected.version, CODEX_WAKE) : false;
615
+ : `${trust.map((hook) => `${hook.event} ${words[hook.trust]}`).join(', ')}${hooksOk ? '' : `. Codex skips a hook you haven’t trusted: ${TRUST_CLI}; ${TRUST_APP}`}`;
616
+ const wakes = detected
617
+ ? atLeast(detected.version, detected.command ? CODEX_APP_WAKE : CODEX_WAKE)
618
+ : false;
587
619
  const lines = [
588
620
  `Pending You for Codex · ${origin}${why ? ` · no browser here (${why})` : ''}`,
589
621
  ` ${mark(signOk)} Sign-in: ${signText}`,
590
- ` ${mark(detected !== null)} Codex: ${detected?.version ?? 'not found on PATH'}`,
622
+ ` ${mark(detected !== null)} Codex: ${detected ? `${detected.version}${detected.command ? ` (${FROM_APP}: ${detected.command})` : ''}` : 'not found on PATH, nor the Codex app'}`,
591
623
  ` ${mark(mcpOk)} MCP server: ${mcpText}`,
592
624
  ...(check
593
625
  ? [
@@ -598,7 +630,11 @@ export const codex = {
598
630
  : []),
599
631
  ` ${mark(skill !== null)} Skill: ${skill !== null ? `saved (${codexSkillPath(io)})` : `not saved; run npx pendingyou init --app codex${flag}`}`,
600
632
  ` ${mark(hooksOk)} Hooks: ${hooksText}`,
601
- ` ${mark(wakes)} Wake: ${wakes ? 'codex queue wakes a thread when you answer, even while it’s idle' : `needs Codex ${CODEX_WAKE} or later${detected ? ` (this is ${detected.version})` : ''}; until then an answer reaches a thread when you next write in it`}`,
633
+ ` ${mark(wakes)} Wake: ${wakes
634
+ ? detected?.command
635
+ ? 'codex queue wakes a chat the Codex app has open when you answer, while the app runs (one it let go, idle 3 hours or behind 10 newer idle chats, hears it when you open it again)'
636
+ : 'codex queue wakes a thread when you answer, even while it’s idle'
637
+ : `needs Codex ${detected?.command ? CODEX_APP_WAKE : CODEX_WAKE} or later${detected ? ` (this is ${detected.version})` : ''}; until then an answer reaches a thread when you next write in it`}`,
602
638
  ];
603
639
  const setup = setupStatus(NAME, signIn.state === 'ok' ? setupOf(signIn.for) : null);
604
640
  if (setup)
@@ -53,10 +53,15 @@ const SOURCES = {
53
53
  resume: 'resume',
54
54
  fork: 'resume',
55
55
  };
56
- /** How the session-start hook's setup lines begin (the command line's setup.ts): these start a turn. */
56
+ /**
57
+ * What the session-start hook's setup lines say (the command line's setup.ts): these start a turn. 0.13.0's, and the
58
+ * words before them (0.12.0's), which a shim run by an older init's copy may still print.
59
+ */
57
60
  export const SETUP_LINES = [
58
- 'Pending You: this computer is connected (',
61
+ ' is connected, but its setup isn’t finished. ',
62
+ ' is connected, but setup’s test question isn’t sent yet. ',
59
63
  'Pending You: your person answered setup’s test question',
64
+ 'Pending You: this computer is connected (',
60
65
  ];
61
66
  /** The most of one string a card's call hands the hook: what a card says, to match an ask against (stopcheck.ts). */
62
67
  const MAX_STRING = 20_000;