pendingyou 0.12.0 → 0.14.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +142 -5
  2. package/README.md +112 -28
  3. package/dist/api.js +0 -50
  4. package/dist/apps/claude-code.js +16 -36
  5. package/dist/apps/codex-app.js +26 -0
  6. package/dist/apps/codex-wake.js +299 -120
  7. package/dist/apps/codex.js +51 -83
  8. package/dist/apps/ids.js +0 -11
  9. package/dist/apps/opencode-plugin.js +39 -65
  10. package/dist/apps/opencode.js +0 -70
  11. package/dist/apps/pi-extension.js +63 -73
  12. package/dist/apps/pi.js +0 -62
  13. package/dist/apps/registry.js +0 -2
  14. package/dist/args.js +45 -39
  15. package/dist/bridge.js +71 -83
  16. package/dist/claude.js +63 -87
  17. package/dist/cli.js +0 -2
  18. package/dist/confirm.js +0 -69
  19. package/dist/credentials.js +0 -27
  20. package/dist/environment.js +0 -28
  21. package/dist/errors.js +0 -1
  22. package/dist/files.js +1 -12
  23. package/dist/format.js +30 -13
  24. package/dist/handed.js +71 -0
  25. package/dist/hold.js +0 -24
  26. package/dist/hooks.js +8 -29
  27. package/dist/index.js +0 -2
  28. package/dist/init.js +2 -33
  29. package/dist/install.js +0 -32
  30. package/dist/io.js +2 -12
  31. package/dist/json.js +0 -29
  32. package/dist/jsonc.js +0 -33
  33. package/dist/main.js +15 -24
  34. package/dist/mod.js +0 -23
  35. package/dist/oauth.js +0 -45
  36. package/dist/permission.js +631 -0
  37. package/dist/pickup.js +109 -63
  38. package/dist/qr.js +0 -28
  39. package/dist/redact.js +292 -0
  40. package/dist/remote.js +0 -90
  41. package/dist/setup.js +8 -27
  42. package/dist/shim.js +8 -24
  43. package/dist/signin.js +0 -42
  44. package/dist/state.js +19 -10
  45. package/dist/stopcheck.js +50 -84
  46. package/dist/toml.js +0 -34
  47. package/dist/version.js +1 -2
  48. package/dist/watch.js +3 -20
  49. package/mod/.claude-plugin/plugin.json +1 -1
  50. package/mod/README.md +2 -2
  51. package/mod/hooks/register.js +109 -62
  52. package/mod/hooks/wake.js +119 -104
  53. package/package.json +2 -1
  54. package/mod/tests/wake.test.ts +0 -247
package/CHANGELOG.md CHANGED
@@ -1,5 +1,142 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.0 (2026-10-05)
4
+
5
+ - Questions your person hands one of your agents from another assistant (Delegate: Pending You's card link, or D on the
6
+ desk). Once the delegation's 5-second hold is over, Pending You's `/mcp/cli/answers` brings such a question to the
7
+ app's sign-in, marked `delegated` (who asked, `freely` or `loop`, the person's note), under the name the helper
8
+ answers with (`sessionLabel`, the task the person picked) and in that task's folder (`cwd`). The hooks used to say
9
+ “Pending You: they wrote back on req_…”; now a session start or message says “Pending You: your person handed you
10
+ req_…, a question from billing-webhooks (answer freely: your answer goes straight to billing-webhooks).”, the
11
+ question, “Their note: “…””, and “Call get_request (requestId req_…, name “infra”) to read it. If you know,
12
+ answer_delegated with your answer and how you know; if not, hand_back with what you checked.” (“they’ll see your
13
+ answer first” when the person kept themselves in the loop). The header says “your person handed you a question from
14
+ another assistant” (or “answered, and handed you …”), never “your person answered”, and the line to tell the person
15
+ first is “You handed me a question from …”.
16
+ - Which session hears one (`src/handed.ts`), the same for every app: a session that goes by its name in its folder
17
+ (the task the person picked; the most recent of several), else the session in that folder that used Pending You
18
+ most recently, never the one that asked it (a session that goes by the asking assistant's name), and never two: the
19
+ session that takes it claims it first in `state.json` (`claimHanded`), and gives it back if it couldn't be told. A
20
+ question with no folder (a task that never said one) goes to a session by its name anywhere, else the app's most
21
+ recent. A sign-in that only hears (`--oauth`) hears every computer's questions, so it leaves one whose `machine`
22
+ (the computer Pending You says it went to, as the Assistants page names it) is another computer than this one, by
23
+ its name or its hostname (the hostname, the names its code sign-ins were given, a Mac's own name, and, outside a
24
+ hook, Tailscale's); one with no `machine` goes by the folder. The hooks hand a session one at once when it goes by that name (or it's the session the listeners would
25
+ wake); another session in the folder only after 5 minutes, so the one it went to hears it first.
26
+ - Claude Code's wake mod wakes the session it's for: while a session used Pending You in the last 12 hours (as long as
27
+ Pending You offers its task as running), it asks `list_pending` with the session's name every minute for the first
28
+ half hour, then every 3 minutes; reads each new one with `get_request` (who asked, how its answer travels, whether
29
+ there's a note); and starts one turn, “Pending You: your person handed you “…” (req_…), a question from … Call
30
+ get_request (requestId req_…, name “infra”) to read it and their note. If you know, answer_delegated …; if not,
31
+ hand_back …”, never with the note. It claims each in its store (`handed:<requestId>`), so another session by the same
32
+ name doesn't hear it too, and never wakes a session for one it read or answered itself. Among sessions by one name,
33
+ the one working in the folder of the task it went to (`list_pending`'s `cwd`: the folder the session started in, or
34
+ one it gave its own cards, `~/…` or full) takes it, one elsewhere never does, and one that can't tell its folder
35
+ waits 5 minutes. The hand-off hook that
36
+ runs for the mod's prompt claims it in `state.json` without saying it twice. `answer_delegated` and `hand_back` are
37
+ among the calls it reads. Regenerated into the plugin's builds too, which reach installed plugins with the next guide
38
+ release.
39
+ - Codex: one listener for the app, `pendingyou listen --app codex --handed` (detached, a lease of its own), started by
40
+ the hooks while any thread may be handed a question, `codex queue`s each into the thread it's for, in the wake mod's
41
+ words, and gives it back (forgetting the thread) when Codex doesn't know the thread. Codex's hooks now remember each
42
+ thread's names and folder (`codex-threads.json`), and keep a thread for 12 hours after it last used the card tools,
43
+ with no card of its own waiting; a session start or message marks it active. A thread's own listener is never woken
44
+ for its own card while it's with another assistant.
45
+ - OpenCode and Pi: a session's listener also wakes it for a question handed to it, and keeps listening for one for 12
46
+ hours after the session last used the card tools (`opencode-threads.json`, `pi-threads.json`), while the app is
47
+ open. OpenCode's plugin starts it with each message while the record says so, and passes the session's folder to
48
+ `posted`; Pi's extension starts it as before, now for that too. Both start a listener again when a card call asked for
49
+ one while the last was on its way out: a card posted just as a session's first listener stopped (finding nothing to
50
+ listen for) went unheard until the next message.
51
+ - `watch` names a handed question's event `delegated_ready` (`PENDINGYOU_EVENT`), with `delegated` in the JSON on
52
+ stdin.
53
+ - The Stop check never asks a helper to post the question as a card of its own. `answer_delegated` and `hand_back`
54
+ count as the turn's card calls (Claude Code's transcript, OpenCode's turn, Pi's extension); a turn a handed
55
+ question's wake started (its first message the wake's words: Claude Code's transcript, OpenCode's turn, Pi's extension
56
+ says `handed`, Codex's next-message hook records it when Codex gives the message) that left the person something in
57
+ chat without either is asked once: “Your person handed you that question to answer, not to ask them back:
58
+ answer_delegated if you know, or hand_back with what you checked, unless you already did. Never post it as a card of
59
+ your own.” Codex's PostToolUse hook is trusted by its exact matcher, which names only the four card tools, so it never
60
+ sees those two calls (a new matcher would have to be trusted again).
61
+ - The package names no one, and carries only what runs. Its JavaScript is built without the source's comments, and so is
62
+ the wake mod's (`mod/hooks`); its examples and this changelog use made-up names (`--name build-01`); and the mod's
63
+ test, which only `claude plugin test` reads, stays out of the package. Nothing the command line does changes.
64
+
65
+ ## 0.13.0 (2026-10-04)
66
+
67
+ - Claude Code's permission prompts on a card: a card and a push, cleared once you answer in the terminal. A Claude Code
68
+ agent kept asking to remove files and sat waiting until its person happened to look. `init` now adds Claude Code's
69
+ PermissionRequest hook (https://code.claude.com/docs/en/hooks: it runs as Claude Code is about to ask for permission
70
+ to use a tool, with `tool_name` and `tool_input`), through the shim with `--app claude-code`.
71
+ It never decides: it prints nothing, so the prompt shows exactly as before, and it's done in about a tenth of a
72
+ second. It writes the prompt down (its card's redacted words, never the tool's input) in the session's own file,
73
+ `~/.config/pendingyou/permission-cards/<session>.json`, and starts that session's one worker, detached (`pendingyou
74
+ permission-card`). Once a prompt has waited 10 seconds (one answered sooner gets no card), the worker posts one card
75
+ through Pending You's MCP with Claude Code's own connection here (whoami, match_area, post_request): an action card
76
+ titled “Claude Code is waiting for your OK: rm -rf dist/” (at most 90 characters; “…waiting for your answer: …” for
77
+ a question it asked, “…on its plan” for a plan), the folder and the computer in its summary, one step, “Answer it in
78
+ Claude Code on Ann-MBP” (with the session's link while Remote Control is on), asked in both places (`askedFirst`
79
+ terminal, at the prompt's time) so a push comes only after the person's hand-off wait, not blocking, urgency now. It
80
+ goes in the area `match_area` picks for the folder and its git remote (a new one for the work when nothing fits, as
81
+ the guide says), under the name the session gave Pending You (read from its transcript), else “Claude Code”, with an
82
+ idempotency key per prompt. One card per session: a newer prompt changes its words (`update_request`, with a
83
+ changeNote), never a second card. It goes once the person answers: the call running (PostToolUse, or
84
+ PostToolUseFailure) withdraws it with `answeredHere`; a denial runs neither, so the person's next message (the
85
+ hand-off hook) or the end of the turn (the Stop hook) does; the session ending (SessionEnd) withdraws it without. A
86
+ card they pressed Done on is acknowledged instead. A subagent's prompt outlasts the person's message, and the end of a
87
+ turn while a subagent still runs in the background. No card where nobody sees the prompt (`claude -p` and the Agent
88
+ SDK, whose CLAUDE_CODE_ENTRYPOINT starts `sdk-`; a dontAsk or bypassPermissions session), for a sign-in that only
89
+ hears (`--oauth`), or on Windows.
90
+ - Nothing secret on those cards (`src/redact.ts`). A command is cut to 120 characters, with the values after anything
91
+ named like a secret blanked (`--token …`, `--password=…`, `-p …` but not `mkdir -p`, `mysql -p…`, `-u user:…`,
92
+ `Authorization: …`, `Cookie: …`, `Bearer …`, `FOO_TOKEN=…`, `"password": "…"`, `aws_secret_access_key …`), a URL's
93
+ user and password and every query and fragment value blanked, and anything that looks like a key or token (the
94
+ well-known prefixes, JWTs, private keys, long hex, long runs of letters and digits in both cases, a webhook's secret
95
+ 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`
96
+ or `bash -c`, `eval`, or one that takes or sets a secret, like `sudo -S`, `--password-stdin` or `gh secret set`)
97
+ shows as the tool and its first word: “Bash: node …”. A path is shown under `~`, its end kept when it's long; a
98
+ question or a search keeps its words with any key taken out. The git remote `match_area` gets has no user name or
99
+ password.
100
+ - `init --no-permission-cards` leaves them out (later runs keep them out; `--permission-cards` puts them back), and
101
+ `status` has a line: “Permission prompts: a card when Claude Code waits for your OK”, off as the person chose, or why
102
+ this Claude Code can't have them. They need Claude Code 2.1.119 or later (PermissionRequest came in 2.0.45, and
103
+ before 2.1.101 a hook event Claude Code didn't know made it ignore the whole settings file) and Claude Code signing
104
+ in through this computer's sign-in. The SessionEnd hook sets no timeout of its own, so Claude Code's 1.5 seconds for
105
+ SessionEnd hooks stays as it was. The shim skips Node for `permission-done` while no session has a prompt written
106
+ down, so a tool call costs a few milliseconds (20 ms measured, 8 of them the shell's own); the PermissionRequest hook
107
+ takes about 0.14 s, as long as the other hooks. `uninstall` takes the hooks and the sessions' files out.
108
+ - The hand-off hook names no permission prompt's card among the agent's open cards, and neither hook hands an agent a
109
+ Done the person pressed on one: those close by themselves.
110
+ - The Codex app for macOS ("ChatGPT.app", bundle id com.openai.codex; 26.928 carries codex 0.159.2), which shares
111
+ `~/.codex` with Codex CLI: its MCP servers (the headers helper too), `~/.agents/skills` and `~/.codex/hooks.json`
112
+ with the same trust. Where `codex` isn't on PATH, `init` and `status` find the app's own codex
113
+ (`Contents/Resources/codex-cli/bin/codex`, or the `CodexCLI.app` the app's own code looks for there, in
114
+ `/Applications` or `~/Applications`, as ChatGPT.app or Codex.app) and run it for the version check (0.152 or later,
115
+ for the headers helper), `codex mcp list` and `codex mcp logout`; init says it found "Codex 0.159.2 (the Codex
116
+ app’s)", and `status` names its path. The listener runs it for `codex queue`: the app's own server reads the queue
117
+ 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
118
+ or behind 10 newer idle chats, hears it when it's opened again), and `status`'s Wake line says so.
119
+ - Codex's last lines name both ways to trust its hooks (“In Codex CLI choose “Trust all and continue” (or trust them in
120
+ /hooks); in the Codex app click Review hooks › Allow all (or Settings › Hooks › Trust all)”; the app alone: open it
121
+ and click Review hooks › Allow all), and say not to press Save on pendingyou in the Codex app's MCP settings: that
122
+ writes the table again without `http_headers_helper` (and `default_tools_approval_mode`), so Codex no longer signs in
123
+ through this computer's sign-in. `status` says so when the table has lost the helper init gave it (“has lost its
124
+ http_headers_helper … (saving it in the Codex app’s MCP settings does that) … run npx pendingyou init --app codex
125
+ again”), and init puts it back (“Put this computer’s sign-in back on Codex’s pendingyou MCP server”). Its Hooks line
126
+ names both ways to trust them.
127
+ - The setup line every app's session start gives, and the Claude Code wake mod's setup turn, say exactly the three
128
+ calls: “Pending You: Codex on Ann-MBP is connected, but its setup isn’t finished. Three calls, nothing to install or
129
+ approve: 1. whoami with your name. If it says app.setUp is true, another session did this: stop. 2. report_setup with
130
+ source codex, your name, skillSaved true, hears instant, and skillVersion set to whoami’s skill.latest. Its answer
131
+ has a testAreaId. 3. post_request a short test question in that testAreaId, tell your person it’s on its way, and end
132
+ your turn: you’re woken when they answer, so don’t ask them to type anything.” Pi's first live setup spent about
133
+ 20,000 tokens on the words before: “whoami with your name and guide true” answered with the whole guide
134
+ (87,000 characters), which Pi cut into a file the model then parsed, and “your test question in its testAreaId” sent
135
+ it looking for testAreaId in whoami's answer, where it isn't (report_setup's has it). A setup that has reported gets
136
+ the same calls without whoami's way out (its whoami says app.setUp is true); a test card answered says who picks it
137
+ 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
138
+ wake mod names the connection from whoami's answer.
139
+
3
140
  ## 0.12.0 (2026-10-04)
4
141
 
5
142
  - The stdio bridge, `pendingyou mcp --app <app>`: a local MCP server for an agent that can't give its MCP client a
@@ -69,7 +206,7 @@
69
206
 
70
207
  ## 0.11.0
71
208
 
72
- - One step for the coding agents on a computer (Darin, 2026-10-04): `npx -y pendingyou@latest init` finds Claude Code
209
+ - One step for the coding agents on a computer: `npx -y pendingyou@latest init` finds Claude Code
73
210
  and Codex (`--app claude-code,codex` names some), says what it found and which it can't set up (an agent too old for
74
211
  it is named, with the version it needs), and sets each up with one approval. Then: restart the agent, and its first
75
212
  session finishes setting up by itself (whoami, report_setup, a test card). Asked for by name and not installed, or
@@ -147,10 +284,10 @@
147
284
  - The next-message hook looks for this folder's answers with every message, not only while a hold had left an entry
148
285
  behind: one request, as before, given up after 2 seconds, inside the hook's 3-second deadline. `state.json` no
149
286
  longer keeps holds' waits.
150
- - A computer with no browser (a devbox or server you SSH into): one code approved on your phone sets Claude Code up.
287
+ - A computer with no browser (a server you SSH into): one code approved on your phone sets Claude Code up.
151
288
  `/mcp`'s Authenticate opened a browser on that machine, which nobody could reach, and `login --device` only signed
152
289
  this command in. Now `init` there (an SSH session, or Linux with no display; `--device` forces it, `--browser` the
153
- browser) signs the machine in by code as a Claude Code connection of its own ("Claude Code on devbox" on your
290
+ browser) signs the machine in by code as a Claude Code connection of its own ("Claude Code on build-01" on your
154
291
  Assistants page, named by `--name`, else Tailscale's name for it, else its hostname; `login --name` with a new name
155
292
  renames the connection with one more approval, which `login` suggests for a cloud's own name like `ip-10-42-1-252`;
156
293
  QR code of the link at a terminal), then adds the MCP server with a `headersHelper` instead of OAuth
@@ -164,8 +301,8 @@
164
301
  PATH), each with the command that fixes it, `--origin` included off production. `uninstall` takes the helper out and
165
302
  says the connection stays on your Assistants page. Needs Pending You's Worker from the same release.
166
303
  - `login` over SSH signs in by code without `--device`. A sign-in by code names the computer, and the phone asks "Let
167
- devbox hear your answers for the assistants running on it?"; in the browser, "Let this computer hear…". Neither offers
168
- a choice of assistant any more (Darin, 2026-10-04): a computer hears for the assistants running on it. A sign-in
304
+ build-01 hear your answers for the assistants running on it?"; in the browser, "Let this computer hear…". Neither
305
+ offers a choice of assistant any more: a computer hears for the assistants running on it. A sign-in
169
306
  pointed at one assistant before then keeps hearing it, so `watch` for a Muse or another app can't be set up anew.
170
307
  - Routing by sender: the hooks and `hold` ask Pending You for Claude Code's cards only (`source=claude-code`), so a card
171
308
  Codex or another app posted from the same folder never reaches a Claude Code session, even with a sign-in pointed at
package/README.md CHANGED
@@ -24,9 +24,11 @@ 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
- through its plugin and Pi through its extension (which also do the hooks' work there).
30
+ through its plugin and Pi through its extension (which also do the hooks' work there); and when you hand it a
31
+ question from another of your assistants ([below](#when-you-hand-an-agent-a-question)).
30
32
 
31
33
  Then it says what to do next, and the one sentence to say if it's needed:
32
34
 
@@ -38,8 +40,9 @@ Finish setting up Pending You: it’s connected here already.
38
40
  ```
39
41
 
40
42
  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.
43
+ test card. Claude Code 2.1.287 or later starts that turn by itself; otherwise (and in Codex, after you trust its hooks:
44
+ “Trust all and continue” in Codex CLI, Review hooks › Allow all in the Codex app) the session start adds one line with
45
+ the three calls that are left, and you say the sentence.
43
46
  `npx pendingyou status` says what's set up for each agent, what's missing with the command that fixes it, and how setup
44
47
  stands. `npx pendingyou uninstall` removes exactly what `init` added.
45
48
 
@@ -92,14 +95,25 @@ default_tools_approval_mode = "approve"
92
95
  ```
93
96
 
94
97
  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.
98
+ knows a turn posted, and so the thread is woken), and the Stop check. **Codex runs a hook only once you trust it**: in
99
+ Codex CLI, start `codex` and choose “Trust all and continue” (or trust them in `/hooks`); in the Codex app, click
100
+ Review hooks › Allow all (or Settings › Hooks › Trust all). `status` says which are trusted. A `pendingyou` server or
101
+ hooks Codex's `/import` copied from Claude Code (without its sign-in) are replaced.
102
+
103
+ **The Codex app** (on a Mac, in the ChatGPT app) shares all of this with Codex CLI: the same `~/.codex`, server, hooks
104
+ and trust. With no `codex` on PATH, `init` uses the app's own (inside `/Applications/ChatGPT.app` or
105
+ `~/Applications`), for the version and everything else it runs. Don't press Save on pendingyou in the app's MCP
106
+ settings: that writes the table again without `http_headers_helper` (and `default_tools_approval_mode`), so Codex stops
107
+ signing in through this computer's sign-in. `status` says so when it happens; `npx -y pendingyou@latest init` puts it
108
+ back.
98
109
 
99
110
  When you answer a card a Codex thread posted, a small listener `init`'s hooks start for that thread puts a message into
100
111
  it with `codex queue`: “Pending You: your person answered “…” (req_…). Call get_request …”, which reads your answer (the
101
112
  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.
113
+ 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
114
+ 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
115
+ you open it again). One more listener, for the whole app, queues a question you hand a thread from another assistant
116
+ ([below](#when-you-hand-an-agent-a-question)).
103
117
 
104
118
  ## OpenCode
105
119
 
@@ -127,7 +141,8 @@ does in OpenCode what the hooks do elsewhere, through the same hooks' script: ea
127
141
  reminder and any answer waiting (hidden in OpenCode's view, read by the model), a session's first message also says
128
142
  what setup has left, a session that left you a question in chat is asked once to put it on a card (with a toast saying
129
143
  why), and when you answer a card a session posted, the plugin starts a turn in that session: “Pending You: your person
130
- answered “…” (req_…). Call get_request …” (never the answer itself). That works while OpenCode is open, idle or busy.
144
+ answered “…” (req_…). Call get_request …” (never the answer itself). That works while OpenCode is open, idle or busy,
145
+ and for a question you hand the session from another assistant too ([below](#when-you-hand-an-agent-a-question)).
131
146
 
132
147
  Then restart OpenCode (`/exit`, then `opencode --continue`): it finishes setting up the first time you write to it.
133
148
 
@@ -155,7 +170,9 @@ Pi has no hooks, so `init` gives it an extension: `~/.config/pendingyou/pi/pendi
155
170
  the same hooks' script as a session starts, with each message, after Pending You's card tools and as a run ends (a run
156
171
  that leaves you something only in chat goes on once), and when you answer a card a session posted it starts a turn in
157
172
  that session, idle or busy: “Pending You: your person answered “…” (req_…). Call get_request …” (never the answer
158
- itself). In Pi's terminal the first session finishes setting up by itself, once Pending You's tools are there.
173
+ itself), and the same for a question you hand it from another assistant
174
+ ([below](#when-you-hand-an-agent-a-question)). In Pi's terminal the first session finishes setting up by itself, once
175
+ Pending You's tools are there.
159
176
 
160
177
  Then run `/reload` in each Pi session that's open, or start `pi`. Pi's shell waits for a command to end, so in Pi
161
178
  `npx pendingyou hold` answers at once instead of waiting.
@@ -196,9 +213,38 @@ replied on a card comes from Codex's own hook for Pending You's tools in that tu
196
213
  action card titled “Confirm in Codex on <computer>: <step>” when it asked you to type something there. OpenCode's plugin
197
214
  and Pi's extension (0.12.0) do the same in theirs.
198
215
 
216
+ A question you hand an agent from another assistant (0.14.0) is never asked to go on a card of its own: answering it
217
+ (`answer_delegated`) or giving it back (`hand_back`) puts it in front of you, so it counts as the turn's card call. A
218
+ turn that such a question started, and that left you something in chat without doing either, is asked once to answer
219
+ it or hand it back instead (about 45 tokens). Codex's hook for Pending You's tools is trusted by its exact list of
220
+ tools, which doesn't name those two, so there the check knows only that the turn started with one (when Codex gives the
221
+ message to its next-message hook), and says “unless you already did”.
222
+
199
223
  **On 0.2.0 with another origin?** Its hooks blocked every message (`There's no "--origin" command`). Run
200
224
  `npx pendingyou@latest init --origin <url>` again to repair them; see [CHANGELOG.md](CHANGELOG.md).
201
225
 
226
+ ## When Claude Code waits for your OK
227
+
228
+ Claude Code stops to ask before it runs something it isn't allowed to run by itself (`rm -rf dist/`, a push, a file
229
+ outside the folder), and waits, however long you're away. Since 0.13.0, when you don't answer within 10 seconds, a card
230
+ says so: “Claude Code is waiting for your OK: rm -rf dist/”, with the folder and the computer, and one step: answer it
231
+ in Claude Code there (with the session's link while Remote Control is on, where you can answer it from your phone).
232
+ 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
233
+ it). It goes away by itself once you answer in Claude Code: allowed, denied, or the session closed. One card per
234
+ session: a newer prompt changes it.
235
+
236
+ The hook (Claude Code's PermissionRequest) never answers for you: it prints nothing, so the prompt shows exactly as
237
+ before, and it's done in about a tenth of a second. A small process in the background posts, changes and withdraws the
238
+ card through Pending You, with Claude Code's own connection here. Nothing secret goes on the card: values after
239
+ anything named like a key, token or password, a URL's user and password and query values, and anything that looks like
240
+ a key are blanked (`curl -H "Authorization: …" https://api.example.com/items?token=…`), and a command that can't be
241
+ shown safely (several lines, inline code like `node -e`, one that takes a secret) shows only as “Bash: node …”. No card
242
+ for `claude -p` or the Agent SDK, where nobody sees a prompt.
243
+
244
+ It needs Claude Code 2.1.119 or later, signing in through this computer's sign-in (not `--oauth`), and isn't on
245
+ Windows yet. Don't want it? `npx pendingyou init --no-permission-cards` (later runs keep it off; `--permission-cards`
246
+ turns it back on). `npx pendingyou status` says which.
247
+
202
248
  ## How Claude Code hears your answer
203
249
 
204
250
  **Claude Code 2.1.287 or later: the wake mod.** There's nothing to run. In each session the mod learns the cards
@@ -235,10 +281,38 @@ allowed and the server has connected). When it isn't, it starts one turn once th
235
281
  Pending You: it’s connected here already.” and exactly what to do. Once per session, in at most three sessions, never
236
282
  two within ten minutes, and never again once Pending You says it's set up.
237
283
 
284
+ ## When you hand an agent a question
285
+
286
+ In Pending You you can hand a card to another of your assistants, a coding task included (Delegate: the card's link,
287
+ or D on the desk): it answers for you (“Answer freely”), or its answer comes back to you first (“Keep me in the loop”),
288
+ or it hands the card back with what it checked. Since 0.14.0 the task you picked hears it the way it hears your
289
+ answers, once the 5-second window to undo is over:
290
+
291
+ - **Which session**: the one that goes by the task's name in its folder (the most recent, when several do); else the
292
+ session in that folder that used Pending You most recently; never the one that asked, and never two. A sign-in that
293
+ only hears (`--oauth`) hears every computer's, so it leaves one Pending You says went to another computer (by its
294
+ name or its hostname); one that names no computer goes by the folder.
295
+ - **Claude Code**: the wake mod looks for questions handed to the session's name (`list_pending`, every minute while
296
+ the session used Pending You in the last half hour, then every 3 minutes, for 12 hours), reads each with
297
+ `get_request`, and starts a turn: “Pending You: your person handed you “Which AWS account for staging?” (req_…), a
298
+ question from billing-webhooks (answer freely: your answer goes straight to billing-webhooks). Call get_request
299
+ (requestId req_…, name “infra”) to read it and their note. If you know, answer_delegated with your answer and how you
300
+ know; if not, hand_back with what you checked.” Among sessions by one name, the one working in the task's folder
301
+ takes it (as `list_pending` gives it, `~/…` or a full path), one in another folder never does, and one that can't
302
+ tell its folder waits 5 minutes.
303
+ - **Codex**: one listener for the app queues it into the thread it's for (`codex queue`, the same words).
304
+ - **OpenCode and Pi**: the session's listener starts the turn through the plugin or extension; a session that used
305
+ Pending You's card tools keeps listening for 12 hours, while the app is open.
306
+ - **Otherwise**: a session start or your next message in that folder brings it, with your note: at once in the
307
+ session that goes by the task's name, after 5 minutes in any other.
308
+
309
+ Your note never goes in a program's arguments, where any other program on the computer could read it: the agent reads
310
+ it with `get_request`.
311
+
238
312
  ## A computer with no browser
239
313
 
240
- On a devbox or server you reach over SSH, an agent's own sign-in opens a browser on that machine, which you can't
241
- reach. `init` signs in with a code there instead: SSH in, and at the prompt there run
314
+ On a computer you reach over SSH (a build server, a machine in the cloud), an agent's own sign-in opens a browser on
315
+ that machine, which you can't reach. `init` signs in with a code there instead: SSH in, and at the prompt there run
242
316
 
243
317
  ```sh
244
318
  npx -y pendingyou@latest init
@@ -255,15 +329,15 @@ hooks find Node anyway when the agents run without your shell's PATH (a systemd
255
329
  Remote Control): on PATH, nvm's default, the one `init` ran with, or where Node usually is.
256
330
 
257
331
  The machine is called by its Tailscale name when Tailscale is installed and running (as `tailscale status` shows it),
258
- else by its hostname, or by `--name devbox` on `init` or `login`; a Mac by its own name (Sharing in System Settings). A
259
- cloud's own hostname (AWS's `ip-10-42-1-252`) is kept when there's nothing better, and `login` says how to change it:
332
+ else by its hostname, or by `--name build-01` on `init` or `login`; a Mac by its own name (Sharing in System Settings).
333
+ A cloud's own hostname (AWS's `ip-10-42-1-252`) is kept when there's nothing better, and `login` says how to change it:
260
334
  `npx pendingyou login --name <name>` asks for one more approval and renames the connection, which stays the same one.
261
335
  If an agent's sign-in ends (30 days, or you remove it on your Assistants page), `npx pendingyou login` (`--app codex`
262
336
  for Codex's) approves a new code and keeps the same connection.
263
337
 
264
338
  Only want this command's own sign-in (for `watch`, below)? `npx pendingyou login --device` signs it in with a code,
265
- hearing answers only, and adds no connection. Your phone asks "Let devbox hear your answers for the assistants running
266
- on it?", with no choice of assistant.
339
+ hearing answers only, and adds no connection. Your phone asks "Let build-01 hear your answers for the assistants
340
+ running on it?", with no choice of assistant.
267
341
 
268
342
  ## Always-on scripts
269
343
 
@@ -275,16 +349,17 @@ npx pendingyou watch -- ./on-answer.sh
275
349
 
276
350
  It hears your Claude Code assistants' answers. Signing in asks no choice of assistant (since 2026-10-04: "Let pi hear
277
351
  your answers for the assistants running on it?"); a sign-in pointed at one assistant before then keeps hearing that
278
- one. The command runs without a shell, once per answer, message or fallback that comes due, oldest first. It
279
- gets the request as JSON on stdin, and `PENDINGYOU_REQUEST_ID`, `PENDINGYOU_EVENT` (`answer_ready`, `message_ready` or
280
- `fallback_due`), `PENDINGYOU_VERSION` and `PENDINGYOU_ORIGIN` in its environment. A failing command is tried twice
281
- more, then skipped. `--once` stops after the first.
352
+ one. The command runs without a shell, once per answer, message or fallback that comes due, or (0.14.0) question you
353
+ hand the assistant from another one, oldest first. It gets the request as JSON on stdin (a handed question with
354
+ `delegated`: who asked, how its answer travels, your note), and `PENDINGYOU_REQUEST_ID`, `PENDINGYOU_EVENT`
355
+ (`answer_ready`, `message_ready`, `fallback_due` or `delegated_ready`), `PENDINGYOU_VERSION` and `PENDINGYOU_ORIGIN` in
356
+ its environment. A failing command is tried twice more, then skipped. `--once` stops after the first.
282
357
 
283
358
  ## Commands
284
359
 
285
360
  | Command | What it does |
286
361
  | --- | --- |
287
- | `init` | Set up every agent on this computer (`--app`, `--oauth`, `--no-login`, `--no-browser`, `--device`, `--browser`, `--yes`, `--name`) |
362
+ | `init` | Set up every agent on this computer (`--app`, `--oauth`, `--no-login`, `--no-browser`, `--device`, `--browser`, `--yes`, `--name`, `--no-permission-cards`) |
288
363
  | `hold <requestId>` | Wait for the answer, print it, exit (`--timeout 4h`); the wake mod answers it itself |
289
364
  | `status` | What's set up for each agent; exits 0 when each hears answers right away (`--app`) |
290
365
  | `login` / `logout` | Sign an agent in again, or out (`--app`, `--device`, `--browser`, `--force`, `--name`, `--all`) |
@@ -311,19 +386,28 @@ Pending You (Claude Code says which server it's for; Codex's is the one `init` w
311
386
  sign-in each agent's connection used, so signing in again keeps it.
312
387
 
313
388
  `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`
389
+ reminded an unfinished setup, and when; `permission-cards/` each Claude Code session's prompts still waiting on your OK
390
+ and its card (ids, hashes and the card's redacted words: never what the tool was given), gone once nothing waits;
391
+ `claude-code.json`, `codex.json`, `opencode.json` and `pi.json` what `init`
315
392
  added; `codex-threads.json`, `opencode-threads.json` and `pi-threads.json` which cards each Codex thread or OpenCode or
316
- Pi session waits on (ids, names and titles: never an answer); `pi/` Pi's extension; and `cli/` the hooks' copy of the
393
+ Pi session waits on, and (0.14.0) the names it went by and its folder, kept 12 hours for a question you may hand it
394
+ (ids, names, folders and titles: never an answer or a note); `pi/` Pi's extension; and `cli/` the hooks' copy of the
317
395
  command line. `init`, `status` and the hooks read `~/.claude.json` (and a folder's `.mcp.json`, and
318
396
  `~/.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
397
+ whether the agent has a `pendingyou` MCP server and where it points. With a sign-in that only hears, the hooks and
398
+ listeners tell this computer's handed questions from another's by its names: its hostname, the names its sign-ins by
399
+ code were given, a Mac's own name (`scutil --get ComputerName`) and, outside a hook, Tailscale's (`tailscale status`). For a permission prompt's card, the worker reads
400
+ the end of the Claude Code session's transcript only for the name the session gave Pending You, and `git remote
401
+ get-url origin` in its folder (sent without any user name or password in it). Nothing is sent anywhere but Pending
402
+ You. OpenCode's plugin reads its session's messages only when it goes idle, for the check that
320
403
  nothing was left in chat, and hands the hooks' script just that turn's words and tool names.
321
404
 
322
405
  The wake mod runs inside Claude Code and reaches Pending You only through the session's own connection, with
323
- `get_request` on the session's own cards (and, in `init`'s copy, `whoami` to see whether setup is finished). It keeps
324
- each session's cards (id, title, the agent's name, where each stands) in Claude Code's store for mods,
325
- `~/.claude/plugins/store/`, and forgets them once they close or after 7 days. It marks the copy acting for a session in
326
- that session's environment (`PENDINGYOU_WAKE_OWNER`).
406
+ `get_request` on the session's own cards (and, in `init`'s copy, `whoami` to see whether setup is finished), and (0.14.0)
407
+ `list_pending` with the session's name and `get_request` on a question you handed it. It keeps each session's cards (id,
408
+ title, the agent's name, where each stands), and which handed questions a session was told of (ids and when), in Claude
409
+ Code's store for mods, `~/.claude/plugins/store/`, and forgets them once they close or after 7 days. It marks the copy
410
+ acting for a session in that session's environment (`PENDINGYOU_WAKE_OWNER`).
327
411
 
328
412
  ## Development
329
413
 
package/dist/api.js CHANGED
@@ -1,32 +1,11 @@
1
- // Calls to Pending You with the command line's sign-in: a fresh access token (refreshed under the credentials lock,
2
- // so a hold and a hook never spend the same refresh token), one retry after a 401, and plain errors.
3
- //
4
- // Hooks never refresh in their own process. A hook has a hard deadline (main.ts), and a refresh cut off halfway can
5
- // end the sign-in: the server rotates the refresh token as it answers, and keeps exactly one earlier token valid
6
- // (@cloudflare/workers-oauth-provider's previousRefreshTokenId). A refresh abandoned before its answer arrives is
7
- // harmless on its own (the stored token is that earlier one, and still works), but if the server finishes it after a
8
- // later refresh has stored a newer token, that newer token stops working and the next refresh gets invalid_grant. So
9
- // a hook that needs a new token starts `pendingyou refresh` detached (background), which runs to the end under the
10
- // lock whatever happens to the hook, and waits for it only briefly; until it lands the hook uses the token it has, or
11
- // gives up and prints its plain fallback.
12
- //
13
- // Since 0.11.0 each app has its own sign-in (credentials.ts), and every call names the app it's for: its sign-in is
14
- // the one used, and a session's request names it as `source` (routing by sender, 2026-10-04), so Pending You returns
15
- // only that app's cards, never one Codex posted from the same folder to a Claude Code session there. `watch` names no
16
- // app: it hands answers to a script, not a session, with Claude Code's sign-in as before.
17
1
  import { DEFAULT_APP } from "./apps/ids.js";
18
2
  import { credentialsPath, readCredential, updateCredential, } from "./credentials.js";
19
3
  import { isLocked } from "./files.js";
20
4
  import { OAuthError, refresh } from "./oauth.js";
21
- /** Refresh this long before the access token runs out. */
22
5
  const EARLY_MS = 60_000;
23
- /** A hook starts a background refresh this long before the access token runs out, and goes on with the old one. */
24
6
  export const SOON_MS = 5 * 60_000;
25
- /** How long a hook waits for a background refresh when it has no working token. */
26
7
  export const REFRESH_WAIT_MS = 1200;
27
- /** An access token with less than this left isn't worth sending. */
28
8
  const USABLE_MS = 10_000;
29
- /** Not signed in on this computer (for this origin), or the sign-in ended: the person runs `pendingyou login`. */
30
9
  export class SignInNeeded extends Error {
31
10
  name = 'SignInNeeded';
32
11
  ended;
@@ -35,26 +14,19 @@ export class SignInNeeded extends Error {
35
14
  this.ended = ended;
36
15
  }
37
16
  }
38
- /** Pending You couldn't be reached, or had a problem: worth trying again later. */
39
17
  export class Unavailable extends Error {
40
18
  name = 'Unavailable';
41
- /** Seconds the server asked to wait, when it said. */
42
19
  retryAfter;
43
20
  constructor(message, retryAfter) {
44
21
  super(message);
45
22
  this.retryAfter = retryAfter;
46
23
  }
47
24
  }
48
- /**
49
- * Refreshes the sign-in under the lock unless it no longer needs it: a token with more than `earlyMs` left (or, when
50
- * `force`d after a 401, a refresh token another process already replaced) is used as it is.
51
- */
52
25
  async function refreshStored(io, origin, stored, options, app) {
53
26
  let ended = false;
54
27
  const updated = await updateCredential(io, origin, async (current) => {
55
28
  if (!current)
56
29
  return null;
57
- // Another process may have refreshed while this one waited for the lock.
58
30
  if (current.refreshToken !== stored.refreshToken && current.expiresAt - EARLY_MS > io.now())
59
31
  return current;
60
32
  if (!options.force && current.expiresAt - options.earlyMs > io.now())
@@ -84,7 +56,6 @@ async function freshToken(io, origin, force, app) {
84
56
  return stored.accessToken;
85
57
  return (await refreshStored(io, origin, stored, { force, earlyMs: EARLY_MS }, app)).accessToken;
86
58
  }
87
- /** What a process starts to refresh an app's sign-in in the background: `--app` only for an app but Claude Code. */
88
59
  export const refreshArgs = (origin, app, force = false) => [
89
60
  'refresh',
90
61
  '--origin',
@@ -92,10 +63,6 @@ export const refreshArgs = (origin, app, force = false) => [
92
63
  ...(app === DEFAULT_APP ? [] : ['--app', app]),
93
64
  ...(force ? ['--force'] : []),
94
65
  ];
95
- /**
96
- * `pendingyou refresh`, what a hook starts in the background: refreshes the sign-in when it runs out within SOON_MS
97
- * (or `force`, after a 401), under the lock, to the end. Quiet; true when there's a sign-in afterwards.
98
- */
99
66
  export async function refreshInBackground(io, origin, force, app = DEFAULT_APP) {
100
67
  const stored = await readCredential(io, origin, app);
101
68
  if (!stored)
@@ -105,7 +72,6 @@ export async function refreshInBackground(io, origin, force, app = DEFAULT_APP)
105
72
  await refreshStored(io, origin, stored, { force, earlyMs: SOON_MS }, app);
106
73
  return true;
107
74
  }
108
- /** A hook's token: never refreshed here (see the top of this file). */
109
75
  async function hookToken(io, origin, force, signal, app) {
110
76
  const stored = await readCredential(io, origin, app);
111
77
  if (!stored)
@@ -113,10 +79,8 @@ async function hookToken(io, origin, force, signal, app) {
113
79
  const left = stored.expiresAt - io.now();
114
80
  if (!force && left > SOON_MS)
115
81
  return stored.accessToken;
116
- // One refresher at a time: a lock held now is a refresh (or a write) already under way.
117
82
  if (!(await isLocked(credentialsPath(io))))
118
83
  io.background(refreshArgs(origin, app, force));
119
- // Still good for a few seconds: this request goes with it, the next one with the new token.
120
84
  if (!force && left > USABLE_MS)
121
85
  return stored.accessToken;
122
86
  const until = io.now() + REFRESH_WAIT_MS;
@@ -130,14 +94,6 @@ async function hookToken(io, origin, force, signal, app) {
130
94
  }
131
95
  throw new Unavailable('The sign-in is being refreshed in the background.');
132
96
  }
133
- /**
134
- * The token `pendingyou mcp-headers` hands an app's MCP server (remote.ts), and the stdio bridge sends with each
135
- * message (bridge.ts). The app runs the helper each time it connects and gives it 10 seconds, and the bridge may be
136
- * stopped at any moment, so, as for a hook, a refresh is never made in this process: within 5 minutes of expiry it
137
- * starts `pendingyou refresh` detached and waits for its token, up to `waitMs`, then uses the old one while it still
138
- * works. A token handed over has at least 10 seconds left; the app runs the helper again on a 401. `force` (the bridge,
139
- * after a 401) refreshes whatever the expiry says, and hands over only the new token.
140
- */
141
97
  export async function helperToken(io, origin, waitMs, app = DEFAULT_APP, force = false) {
142
98
  const stored = await readCredential(io, origin, app);
143
99
  if (!stored)
@@ -159,7 +115,6 @@ export async function helperToken(io, origin, waitMs, app = DEFAULT_APP, force =
159
115
  return stored.accessToken;
160
116
  throw new Unavailable('Pending You couldn’t be reached to refresh the sign-in.');
161
117
  }
162
- /** Aborts on `signal` or after `ms`, whichever comes first (AbortSignal.any needs Node 20.3). */
163
118
  function either(signal, ms) {
164
119
  const timeout = AbortSignal.timeout(ms);
165
120
  if (!signal)
@@ -172,11 +127,6 @@ function either(signal, ms) {
172
127
  timeout.addEventListener('abort', abort, { once: true });
173
128
  return controller.signal;
174
129
  }
175
- /**
176
- * GETs `path` on `origin` as the command line, with an app's sign-in (Claude Code's unless `app` says), and returns the
177
- * JSON body with its status. 401 after a refresh means the sign-in is over (revoked, or 30 days passed). Network
178
- * failures and 5xx are `Unavailable`.
179
- */
180
130
  export async function getJson(io, origin, path, timeoutMs, signal, options = {}) {
181
131
  const app = options.app ?? DEFAULT_APP;
182
132
  for (let attempt = 0;; attempt++) {