projmux 0.16.0 → 0.16.1

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/docs/hooks.md CHANGED
@@ -15,7 +15,7 @@ projmux-owned internal tmux hooks such as `pane-focus-in`, `pane-focus-out`,
15
15
  | `pre-create` | Before projmux creates a missing persistent or ephemeral session | Non-zero exit, exec error, or timeout aborts creation | Logged with `[pre-create] ` |
16
16
  | `post-create` | After projmux creates a brand-new persistent or ephemeral session | Logged and ignored; creation continues | Logged with `[post-create] ` |
17
17
  | `post-attach` | After projmux switches the current tmux client to an existing session/target from inside tmux | Logged and ignored | Logged with `[post-attach] ` |
18
- | `send-noti` | After `projmux create notification` (or the in-process AI notify producer) successfully writes a queue entry | Fired asynchronously and best-effort; queue write and desktop notifications continue even if the hook fails or times out | Receives JSON on stdin; stdout/stderr are logged with `[send-noti] ` |
18
+ | `send-noti` | After `projmux create notification` (or the in-process AI notify producer) successfully writes a queue entry | Runs after the queue entry is written; the command waits until the hook exits or is killed at its timeout, and a failure or timeout is only a warning that changes neither the queue entry nor the exit code | Receives JSON on stdin; stdout/stderr are logged with `[send-noti] ` |
19
19
 
20
20
  Deferred Phase A candidates remain future work until their behavior can be
21
21
  specified without exposing projmux's internal tmux hook machinery: pane exit,
@@ -35,9 +35,18 @@ Project-local hooks are discovered from the lifecycle context's `PROJMUX_CWD`:
35
35
  <repo>/.projmux/config.toml
36
36
  ```
37
37
 
38
- File-form hooks from the historical `.projmux/<event>` and
39
- `.projmux/hooks/<event>` layouts are no longer executed. Use declarative
40
- `[hooks.<event>] run` entries instead.
38
+ projmux reads `.projmux/config.toml` directly in that directory. It does not
39
+ look in parent directories.
40
+
41
+ For `send-noti`, that directory is resolved as described in
42
+ [Send Noti](#send-noti-working-directory): when `PROJMUX_CWD` is not inherited, it is the nearest
43
+ `.projmux` or `.git` root above the notifying process's working directory.
44
+
45
+ File-form hooks from the historical global
46
+ `${XDG_CONFIG_HOME:-$HOME/.config}/projmux/hooks/<event>` layout and the project
47
+ `.projmux/<event>` and `.projmux/hooks/<event>` layouts are no longer executed.
48
+ Use declarative `[hooks.<event>] run` entries instead. projmux converts or warns
49
+ about these files; see [Troubleshooting](#troubleshooting).
41
50
 
42
51
  If both a global hook and a project-local hook exist for an event, projmux runs
43
52
  the global hook first, then the project-local hook.
@@ -145,8 +154,9 @@ session-level tmux environment before the startup command is sent. It does not
145
154
  run when projmux attaches to an existing session or target.
146
155
 
147
156
  Before sending the startup command, projmux polls tmux `pane_current_command`
148
- for the new pane and waits until it reports a shell command. This avoids fixed
149
- sleeps as the primary readiness mechanism.
157
+ for the new pane and waits up to 2 seconds for it to report a shell command. If
158
+ no shell appears in that time, the startup command is not sent. This avoids
159
+ fixed sleeps as the primary readiness mechanism.
150
160
 
151
161
  The configured string is sent into the new pane:
152
162
 
@@ -162,9 +172,10 @@ An empty `[startup] run` is a no-op.
162
172
  already durable before the hook starts, so a failing or slow hook cannot drop
163
173
  the notification or block the normal desktop notification flow.
164
174
 
165
- The hook runs asynchronously with the same default timeout (`5s`) as other
166
- hooks. projmux does not wait for completion before returning from
167
- `projmux create notification`.
175
+ The hook runs with the same default timeout (`5s`) as other hooks. projmux
176
+ waits until the hook exits or is killed at that timeout before returning from
177
+ `projmux create notification`; a failure or timeout is logged as a warning and
178
+ does not change the exit code.
168
179
 
169
180
  stdin receives one JSON object:
170
181
 
@@ -204,6 +215,33 @@ If a `send-noti` hook itself calls `projmux create notification`, projmux sees
204
215
  `PROJMUX_NOTIFY_HOOK_DEPTH=1` in the child environment and skips another
205
216
  `send-noti` hook fire. The queue write itself still succeeds.
206
217
 
218
+ ### Send Noti Working Directory
219
+
220
+ The `PROJMUX_CWD` a `send-noti` hook receives is resolved by the projmux
221
+ process that queues the notification, whether that is
222
+ `projmux create notification` or projmux queuing an AI agent notification, in
223
+ this order:
224
+
225
+ 1. If that process's environment has a `PROJMUX_CWD` that is non-empty after
226
+ trimming whitespace, projmux uses the trimmed value as is. It does not walk
227
+ up for markers or check that the directory exists.
228
+ 2. Otherwise projmux reads its working directory and uses the nearest
229
+ directory, starting at the working directory itself and walking up to the
230
+ filesystem root, that contains a `.projmux` or `.git` entry (file or
231
+ directory).
232
+ 3. If no such directory exists, projmux uses the working directory itself.
233
+ 4. If the working directory cannot be read or is empty, the value is empty.
234
+
235
+ The same value is the hook command's working directory and the only directory
236
+ whose `.projmux/config.toml` is read for a project `send-noti` hook; projmux
237
+ does not look in its parents. From a subdirectory of a repository, the project
238
+ hook therefore comes from the repository root's `.projmux/config.toml`. When
239
+ the value is empty, no project config is read and the hook runs in the
240
+ dispatching process's working directory.
241
+
242
+ A `projmux create notification` run from inside another hook inherits that
243
+ hook's `PROJMUX_CWD`, so it takes rule 1.
244
+
207
245
  `Settings > Notifications > Delivery sources` surfaces the active Codex,
208
246
  Claude, Antigravity, and tmux AI notify diagnostics: status, conflicts, config
209
247
  paths, and copyable CLI install/remove/dry-run commands. It also shows whether
@@ -280,7 +318,7 @@ not semantic overrides.
280
318
  | `PreToolUse` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
281
319
  | `UserPromptSubmit` | marks the matched pane hook-active and sets AI state to thinking/busy; no notify queue entry is pushed |
282
320
  | `PermissionRequest` | pushes a critical approval row with the tool name and a concise tool/action summary |
283
- | `PostToolUse` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
321
+ | `PostToolUse` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed; while `agent-approval-answering` is `projmux` it also closes the one waiting permission request the terminal answered (see [below](#answering-claude-permission-requests-in-projmux)) |
284
322
  | `PreCompact` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
285
323
  | `PostCompact` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
286
324
  | `SessionStart` | marks the matched pane hook-active and writes a quiet ingest diagnostic; for an exact managed initial-task binding it records `pending` startup readiness and opens the separately bounded acknowledgement window, but never acknowledges the task; no notify queue entry is pushed |
@@ -495,7 +533,7 @@ default install catalog is based on Claude Code 2.1.140 and represents the
495
533
  | --- | --- |
496
534
  | `PreToolUse` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
497
535
  | `PostToolUse` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
498
- | `PostToolUseFailure` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
536
+ | `PostToolUseFailure` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed; while `agent-approval-answering` is `projmux` it also closes the one waiting permission request the terminal answered (see [below](#answering-claude-permission-requests-in-projmux)) |
499
537
  | `PostToolBatch` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
500
538
  | `PermissionDenied` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
501
539
  | `Notification` | pushes a Claude notify row for response-ready, approval-required, or input-ready based on `notification_type` |
@@ -523,6 +561,15 @@ default install catalog is based on Claude Code 2.1.140 and represents the
523
561
  | `CwdChanged` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
524
562
  | `FileChanged` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
525
563
 
564
+ `Stop` is the first transcript tail reader. It opens the `transcript_path` its
565
+ own payload reports, reads at most the last 256 KiB, and keeps one string: the
566
+ text of the last assistant line in that tail. That text becomes the completion
567
+ row's text, so it goes wherever the row goes: the notify queue, the desktop
568
+ notification (whose duplicate check keeps a normalized copy in a tmux pane
569
+ option), and the `send-noti` payload. It is not written to the Registry or to
570
+ the Agent session history, and nothing else the read saw is kept. With no
571
+ readable path or no assistant text, the row text is `Ready`.
572
+
526
573
  Current Claude Code (2.1.277) sends no hook when the operator denies a
527
574
  permission dialog, so neither `PermissionDenied` nor `Stop` closes it and the
528
575
  Agent keeps its `approval_required` observation. The held coordination message
@@ -533,6 +580,17 @@ at each line's type, subtype, and timestamp and whether an assistant line has a
533
580
  `tool_use` item. Nothing it reads is stored, logged, or forwarded. See
534
581
  [held messages](claude-coordination-endpoints.md#held-while-the-target-awaits-its-operator).
535
582
 
583
+ A permission dialog or an elicitation raises its Agent once, so the Agent's
584
+ pane supervisor is a third transcript tail reader. While the Agent's
585
+ interaction is a provider-hook `approval_required` or `input_required`, about
586
+ every ten minutes it reads the same bounded tail of the Agent's own recorded
587
+ `transcript_path`. While the turn has not ended and an assistant `tool_use`
588
+ still has no `tool_result`, it recommits the same interaction, so an open
589
+ dialog does not decay to `unknown` after thirty minutes. It looks only at each
590
+ line's type, subtype, and timestamp, the `tool_use` ids, and the `tool_result`
591
+ `tool_use_id`s. Nothing it reads is stored, logged, or forwarded. A denied or
592
+ dismissed dialog ends the turn, and it stops.
593
+
536
594
  Hook-generated queue rows use the same compact body catalog: agent label,
537
595
  event category, then the best available summary (Codex assistant text, Claude
538
596
  tool/action summary, transcript summary, error, or teammate labels). Structured
@@ -546,9 +604,12 @@ Pane matching follows the shared AI ingest order: inherited `$TMUX_PANE`, then
546
604
  payload `cwd`, then cached session id pane options. A matched pane is marked
547
605
  with `@projmux_ai_hook_active=1`, so `projmux internal agent-hook watch-title` skips the pane
548
606
  after a minimal hook-active gate instead of polling pane title/capture output;
549
- hook payloads become the primary signal. The tmux bell fallback does not mark
550
- panes hook-active, so title/capture fallback remains available for panes that
551
- only emit bells.
607
+ hook payloads become the primary signal. The hook reads the pane's current
608
+ markers once and writes only the ones that differ, so a hook on an already
609
+ marked pane sends tmux no `set-option`: every option write redraws all clients,
610
+ and the first redraw of a second re-runs the status line's `#()` jobs. The tmux
611
+ bell fallback does not mark panes hook-active, so title/capture fallback
612
+ remains available for panes that only emit bells.
552
613
 
553
614
  The Claude hook payload is intentionally accepted directly at the ingest
554
615
  boundary. Core identity fields accept `hook_event_name`/`event_name`,
@@ -697,36 +758,131 @@ Codex and are managed from `Settings > Notifications > Agent event behavior`.
697
758
  They only affect ingest delivery; `projmux agent integrate claude` still uses the
698
759
  catalog `install` field for installed hook events.
699
760
 
700
- ### Answering AskUserQuestion From The Command Line
761
+ ### Answering AskUserQuestion In projmux
701
762
 
702
763
  `projmux agent integrate claude` also installs one `PreToolUse` entry with
703
764
  `"matcher": "AskUserQuestion"` that runs
704
765
  `projmux internal claude-question-hook` (marker
705
- `projmux-managed:claude-question:v1`, `"timeout": 315`). Unlike the ingest
706
- command its stdout is not discarded, because that is where an answer is handed
707
- to Claude Code. Re-running the integration keeps exactly one such entry,
708
- `--remove` deletes it, and `config apply` never adds or changes it.
709
-
710
- The hook does nothing unless the Claude Agent that asks was opted in:
766
+ `projmux-managed:claude-question:v1`, `"timeout"` the fixed ceiling `604800`
767
+ seconds (7 days), which outlasts every answer window including `unlimited`;
768
+ it is the safety net for a stuck hook, which has no recover, and is 7 times
769
+ the longest bounded window, so it never cuts a normal one). Unlike the
770
+ ingest command its stdout is not discarded, because that is where an answer is
771
+ handed to Claude Code. Re-running the integration keeps exactly one such entry,
772
+ `--remove` deletes it, and `config apply` never adds or changes it. An entry
773
+ installed by an older projmux carries an old timeout, the window plus 15
774
+ seconds (`915` by default) or the earlier, longer ceiling of about 25 days;
775
+ run `projmux agent integrate claude` once after upgrading
776
+ to rewrite it to the ceiling.
777
+
778
+ A question is answered one of two ways:
779
+
780
+ | Way | When | What happens |
781
+ | --- | --- | --- |
782
+ | 1: Claude Code's prompt (default) | the Agent is not opted in and `agent-question-answering` is not `projmux` | the hook prints nothing and exits at once; Claude Code shows its usual question prompt |
783
+ | 2: projmux | the Agent is opted in, or `agent-question-answering` is `projmux` | the hook records the question, opens a projmux picker popup, and takes the first answer from the popup or the command line |
784
+
785
+ The Agent's own switch wins over the setting, so an opted-in Agent is always
786
+ way 2:
711
787
 
712
788
  ```sh
713
789
  projmux agent question enable <agent-ref>
714
790
  projmux agent question disable <agent-ref>
715
791
  ```
716
792
 
717
- For every other Agent, and for a subagent's question, the hook prints nothing
718
- and exits at once, so Claude Code shows its usual question prompt.
719
-
720
- For an opted-in Agent the hook records the question and holds the tool call
721
- open for up to 300 seconds. While it waits, Claude Code shows the hook's status
722
- message instead of the question prompt; pressing Esc cancels the wait and
723
- declines the question. Answer it from any shell:
793
+ The setting is the central
794
+ `${XDG_CONFIG_HOME:-$HOME/.config}/projmux/agent-question-answering` file (see
795
+ [configuration.md](configuration.md#agent-question-answering)). The hook reads
796
+ it only after the Registry shows that the question comes from a projmux
797
+ Claude Agent's own conversation; a session projmux did not start, and a
798
+ subagent's question, are always way 1 and never read it.
799
+
800
+ In way 2 the hook holds the tool call open for the answer window: 900 seconds
801
+ unless `${XDG_CONFIG_HOME:-$HOME/.config}/projmux/agent-question-window-seconds`
802
+ holds another value in 60–86400 (see
803
+ [configuration.md](configuration.md#agent-question-window)). The window binds
804
+ only in way 2; way 1 never waits. While it waits, Claude Code shows the hook's
805
+ status message instead of the question prompt.
806
+ A question projmux holds also shows the Agent as `input_required` until the tool runs, however long it waits.
807
+
808
+ The hook opens a tmux popup right away on the terminal you are looking at: the
809
+ client of the Agent's tmux server that you used most recently, whatever
810
+ Session, Window, or Pane it shows. Clients of other tmux servers are not
811
+ considered. The popup's title names the asking Agent and its Project/Window,
812
+ for example `Agent question from reviewer (alpha/main)`, and it runs the projmux
813
+ picker, one question at a time. Enter picks an option of a single-select
814
+ question; on a multi-select question Enter toggles an option and `Done`
815
+ finishes, with at least one option chosen; `Other / type an answer` takes free
816
+ text. When no client is attached the hook keeps waiting and looks again about
817
+ once a second, and opens the popup when one attaches. Nothing is drawn on
818
+ Claude Code's own terminal.
819
+
820
+ Questions show one at a time per client. tmux does not draw a popup over
821
+ another one, so a question that arrives while that client already shows a
822
+ popup (another Agent's question, or any other popup) keeps waiting and shows
823
+ once that popup closes. A popup that tmux did not draw is tried again, never
824
+ counted as ended.
825
+
826
+ A long question wraps inside the popup instead of being cut, and a line break
827
+ in the question starts a new line. The popup is 80% × 70% of the client, but at
828
+ least 72 columns × 22 rows (or the whole client if that is smaller). Only on a
829
+ client smaller than that is the question shortened with `…` so the options stay
830
+ reachable; the full text is available from `projmux agent question list`.
831
+
832
+ Pressing Esc in the popup gives the question back: the record is closed and
833
+ Claude Code shows its own prompt. So does a picker that fails. A popup that
834
+ ends any other way while the question still waits, most often because its
835
+ client detached, does not give the question back: the question stays in
836
+ projmux, and the next look, within about a second, opens the popup again on the
837
+ client you used most recently, or on the first client to attach when none is
838
+ left. Only a popup that keeps failing gives the question back: three popups in
839
+ a row that each ended within 2 seconds of opening, a popup that failed to open
840
+ among them. A popup that stayed up longer starts that count again, so leaving
841
+ clients never runs it out. Pressing Esc in Claude Code itself cancels the wait
842
+ and declines the question.
843
+
844
+ A question is given back only once its record is written `closed`. While the
845
+ question store cannot be written (the disk is full, or its lock is held), Esc
846
+ in the popup cannot close the record, which still waits, so the popup opens
847
+ again. When the hook gives the question back after popups that kept failing,
848
+ the record stays `waiting` and the hook keeps holding the question, so it is in
849
+ neither a popup nor Claude Code's prompt; the hook tries again every 250
850
+ milliseconds (each try waits up to 2 seconds for the store lock) and hands the
851
+ question back as soon as the write succeeds, so the record's `updatedAt` is
852
+ that moment, not the moment the last popup ended.
853
+
854
+ A `closed` record says why it closed in its `disposition`, and that reason
855
+ tells whether the provider still asks the question in its own prompt:
856
+
857
+ | Disposition | Written when | Provider still asks |
858
+ | --- | --- | --- |
859
+ | `popup-dismissed` | Esc in the popup | yes |
860
+ | `popup-failed` | the picker failed; for a Claude question, three popups in a row failed to open or ended within 2 seconds of opening; for a Codex question, the popup failed to open or ended without an answer (its client detached) | yes |
861
+ | `hook-canceled` | Claude Code canceled the hook (Esc in Claude Code, or its hook timeout), which declines the question | no |
862
+ | `hook-failed` | the hook crashed after it recorded the question | yes |
863
+ | `channel-off` | `agent question disable` | yes |
864
+ | `turn-ended` | the Codex turn that asked it ended | no |
865
+ | `watch-stopped` | projmux's Codex observer stopped watching the request (its connection to Codex ended, or the observer stopped) | yes |
866
+ | `answered-elsewhere` | Codex's own input surface answered it first | no |
867
+
868
+ `agent question list` shows the reason and says whether the provider still
869
+ asks, for example `closed (popup-failed; Claude Code still asks it in its own
870
+ prompt)` or `closed (hook-canceled; Claude Code no longer asks it)`; `-o json`
871
+ has the reason in `disposition`. A record closed by an older projmux has no
872
+ reason, and a reason this projmux does not know is shown as written. The
873
+ `question-closed` refusal of `answer` carries the same words after the reason.
874
+
875
+ The same question can be answered from any shell:
724
876
 
725
877
  ```sh
726
878
  projmux agent question list <agent-ref> [-o json]
727
879
  projmux agent question answer <agent-ref> <question-id> --option 1=<label> --index 2=<n> --text 3=<free text>
728
880
  ```
729
881
 
882
+ Whichever answer lands first wins. An answer from the command line closes the
883
+ popup, and a popup answer that arrives after it is refused as
884
+ `question-not-pending`.
885
+
730
886
  Questions are numbered from 1 in the order Claude asked them, and so are the
731
887
  options of each question. `--option <n>=<label>` picks an option by its exact
732
888
  label, `--index <n>=<k>` by its number, and `--text <n>=<text>` answers with
@@ -736,13 +892,165 @@ occurrences, which are joined with `", "` in option order; a single-select
736
892
  question takes exactly one. Every question needs an answer. A refused answer
737
893
  changes nothing and names one reason token: `question-not-found`,
738
894
  `question-not-pending` (already answered), `question-expired`,
739
- `question-closed`, `question-invalid-answer`, `question-channel-off`, or
740
- `question-provider-unsupported`.
895
+ `question-closed`, `question-invalid-answer`, `question-channel-off` (way 1),
896
+ or `question-provider-unsupported`.
897
+
898
+ If nobody answers within the window, the question expires, the popup closes,
899
+ and Claude Code shows its own prompt as usual. The hook reads the window for
900
+ every question and the installed timeout is the fixed ceiling, so a window
901
+ changed with `projmux config agent-questions` or in the file applies to the
902
+ next question without re-running `projmux agent integrate claude`. `agent
903
+ question disable` also hands every question the Agent is still holding back to that
904
+ prompt immediately. Records live in `<state dir>/agent-questions/` and settled
905
+ ones are kept for a day.
906
+
907
+ The hook never blocks the tool: every failure, and even a crash inside the
908
+ hook, ends with no output and exit status 0, which Claude Code reads as no
909
+ decision.
910
+
911
+ ### Answering Claude Permission Requests In projmux
912
+
913
+ `projmux agent integrate claude` also installs one more `PermissionRequest`
914
+ entry, separate from the ingest entry on the same event, that runs
915
+ `projmux internal claude-permission-hook` (marker
916
+ `projmux-managed:claude-permission:v1`, no matcher, `"timeout"` the fixed
917
+ ceiling `3615` seconds, the longest window plus 15 seconds). Unlike the ingest
918
+ command its stdout is not discarded, because that is where a decision is handed
919
+ to Claude Code. The ingest entry, and the `approval_required` row and badge it
920
+ raises, are unchanged. Re-running the integration keeps exactly one such entry,
921
+ `--remove` deletes it, and `config apply` never adds or changes it.
922
+
923
+ The hook decides nothing unless the central `agent-approval-answering` setting
924
+ is `projmux` (see
925
+ [configuration.md](configuration.md#agent-approval-answering)). The hook
926
+ captures Claude permission requests only; Codex approvals are held by the
927
+ Codex app-server and never captured (see [Codex answers](#codex-answers)
928
+ below). In the default `claude` way the
929
+ hook reads the payload and the Registry, and only once the Registry confirms a
930
+ projmux Claude Agent the one setting file; it prints nothing, records nothing,
931
+ and touches no tmux. A session projmux did not start never reads the setting.
932
+ A request in `bypassPermissions` or `dontAsk` mode is never captured. A
933
+ subagent's request is captured, and its `agent_type` is shown by `agent
934
+ approval list` and written to the audit log.
935
+
936
+ In way `projmux` the hook records the request and waits up to the answer
937
+ window (900 seconds unless `agent-approval-window-seconds` holds another value
938
+ in 60–3600). Claude Code shows its own prompt at the same time, and that prompt
939
+ stays fully usable: whichever answer comes first wins.
940
+
941
+ ```sh
942
+ projmux agent approval list <agent-ref> [-o json]
943
+ projmux agent approval answer <agent-ref> <request-id> --allow|--deny [--via popup|cli|<client>]
944
+ ```
741
945
 
742
- If nobody answers within 300 seconds, the question expires and Claude Code
743
- shows its own prompt as usual. `agent question disable` also hands every
744
- question the Agent is still holding back to that prompt immediately. Records
745
- live in `<state dir>/agent-questions/` and settled ones are kept for a day.
946
+ `list` shows each waiting request with its id, tool name, subagent type, full
947
+ tool input, and created and deadline times. A listed request may already have
948
+ been answered in the terminal. `answer` takes exactly one of `--allow` or
949
+ `--deny`:
950
+
951
+ - `--allow` prints `{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}`.
952
+ It allows that one tool call once, exactly as requested: it never carries
953
+ `updatedInput` or `updatedPermissions`, so no permission rule changes, and the
954
+ request's `permission_suggestions` are never applied.
955
+ - `--deny` prints a deny decision with the message `denied by the operator in
956
+ projmux` and `"interrupt": false`.
957
+
958
+ The hook is deny-by-default: it prints the allow decision only for a request
959
+ the store settled as allowed under its lock. Everything else prints nothing
960
+ and exits 0, which Claude Code reads as no decision: no answer before the
961
+ window ends (the request expires), a store that cannot be opened, written, or
962
+ read, a cancellation (SIGTERM, SIGINT, SIGHUP), a crash inside the hook, and a
963
+ malformed or oversized payload. It never exits 2.
964
+
965
+ A second or late answer is refused and changes nothing, with one reason token:
966
+ `permission-not-pending` (already allowed or denied, or already answered in
967
+ Claude Code's own prompt), `permission-expired`, `permission-not-found`,
968
+ `permission-answering-off` (the setting is `claude`), or
969
+ `permission-provider-unsupported` (neither a Claude nor a Codex Agent; nothing
970
+ is written).
971
+
972
+ Answering in the terminal: "No" or Esc sends the waiting hook SIGTERM, which
973
+ closes its record. "Yes" does not tell the hook at all, so the Claude ingest of
974
+ the `PostToolUse` or `PostToolUseFailure` that follows closes the one waiting
975
+ record of the same session, tool, and tool input (compared as canonical JSON)
976
+ with the reason `answered-in-terminal`; the hook then exits with no output.
977
+ When no record or more than one matches, nothing is closed and the window
978
+ expiry ends the wait. In the default way that ingest reads only the setting,
979
+ and with no store file it opens nothing. Known limit: an answer from projmux
980
+ that the store records after the terminal "Yes" but before its `PostToolUse`
981
+ is still accepted and printed. Claude Code has already run the tool by then
982
+ and ignores the late output, but the audit log shows that projmux answer.
983
+
984
+ `--via` is reported by the caller and recorded as given; projmux does not
985
+ verify it. Every event (`requested`, `allowed`, `denied`, `expired`, `closed`,
986
+ `refused` for a late or invalid answer, and `uncommitted` for an answer that
987
+ did not take effect) is appended as one JSON line to
988
+ `<state dir>/agent-approvals/audit.jsonl` (mode 0600, rotated to one `.1`
989
+ generation at 1 MiB) with the request id, Agent, Pane, session, subagent type,
990
+ tool name, a bounded one-line input summary (Bash `command`, a file tool's
991
+ path, otherwise only the input's key names), request and decision times in
992
+ UTC, and `via`. The full tool input lives only in the request record under
993
+ `<state dir>/agent-approvals/`, and settled records are kept for a day.
994
+
995
+ An answer's `allowed` or `denied` line is written and synced before the answer
996
+ takes effect. If that line cannot be written, `answer` fails with an error
997
+ naming the audit log and the request stays waiting: fix the log and answer
998
+ again, or answer in Claude Code. The other events stay best effort and never
999
+ fail the transition they describe. If the line was written but the request
1000
+ record write then fails before the new record replaces the old one, the answer
1001
+ did not take effect: `answer` fails, the request stays waiting, and an
1002
+ `uncommitted` line with `reason` `record-write-failed` follows the answer line
1003
+ for the same request id. If that `uncommitted` line cannot be written either,
1004
+ `answer` says so and the log keeps the `allowed` or `denied` line alone. If
1005
+ the new record is already in place and only the sync of its directory fails,
1006
+ the write took effect: `answer` succeeds with a warning on stderr, the hook
1007
+ keeps waiting on a request it recorded, and the `requested`, `expired`, and
1008
+ `closed` lines are written as usual, but that write may not survive a power
1009
+ loss. No `uncommitted` line is written when the answer took effect, or when its
1010
+ outcome is unknown, such as a crash between the line and the record write. No
1011
+ line marks that crash afterwards: the request stays waiting, and a later
1012
+ `expired`, `closed`, `allowed`, or `denied` line for the same request id shows
1013
+ how it actually ended. For example, `requested`, `allowed`, `denied` for one
1014
+ request id means the first answer never took effect and the request was denied;
1015
+ `requested`, `allowed`, `expired` means it expired without a decision. If the
1016
+ hook died too, as when the machine went down, no later line may follow and the
1017
+ answer's outcome stays unknown. A write killed before its rename can leave a
1018
+ `.requests.tmp-*` file in `<state dir>/agent-approvals/`; the next write of the
1019
+ store removes it. Read the log this way: an `allowed` or `denied` line followed
1020
+ by an `uncommitted` line for the same request id did not take effect, and
1021
+ otherwise the last `expired`, `closed`, `allowed`, or `denied` line for a
1022
+ request id shows how it ended. The log may over-report an answer, never
1023
+ under-report one.
1024
+
1025
+ #### Codex Answers
1026
+
1027
+ The same `list` and `answer` also work for a Codex Agent with an exact native
1028
+ control binding (see [cli-guide.md](cli-guide.md)). projmux stores no Codex
1029
+ request: the provider holds it, so the audit log gets only the `allowed` or
1030
+ `denied` line of an answer sent from projmux and, when that answer is
1031
+ confirmed not to have reached Codex, its `uncommitted` line; never
1032
+ `requested`, `expired`, `closed`, or `refused`. That line carries the normalized request id, Agent,
1033
+ Pane, the approval kind as the tool name, a bounded input summary (a command
1034
+ approval's command, otherwise only the detail key names), `via`, the decision
1035
+ time, and the provider decision actually sent in `reason` as
1036
+ `decision=accept`, `decision=decline`, or `decision=cancel`. `--deny` sends
1037
+ `decline` when the request offers it and otherwise `cancel`, which also
1038
+ interrupts the turn; nothing runs either way. The line is written and synced
1039
+ before that one decision is sent; if it cannot be written nothing is sent and the request stays
1040
+ pending. If the send then fails and the decision is confirmed not to have
1041
+ reached Codex, an `uncommitted` line with `reason` `send-failed` follows the
1042
+ answer line and `answer` says the answer did not take effect. That is the case
1043
+ when the request never left projmux (the binding fence refused it, or the
1044
+ control socket could not be reached) or when the control server refused it
1045
+ before claiming the approval (`stale-epoch`, `stale-binding`, `unavailable`,
1046
+ `invalid-frame`, `ambiguous-request`, or `unsafe-decision`). Any other send
1047
+ failure may have reached Codex: the line stays alone and `answer` says the
1048
+ decision may or may not have reached Codex, so the log may over-report a Codex
1049
+ answer too. If the `uncommitted` line cannot be written, `answer` says so and
1050
+ the log keeps the answer line alone. A crash of `answer` between the line and
1051
+ the send also leaves the answer line alone, and no later line follows it: read
1052
+ it like a send that may or may not have reached Codex. An answer given in the
1053
+ Codex TUI or with `agent approval review` is not logged.
746
1054
 
747
1055
  ## Antigravity Hook Ingest
748
1056
 
@@ -872,7 +1180,7 @@ empty.
872
1180
  | Variable | `pre-create` | `post-create` | `post-attach` | `send-noti` |
873
1181
  | --- | --- | --- | --- | --- |
874
1182
  | `PROJMUX_SESSION` | new session name | new session name | target session name | target session when known; otherwise empty |
875
- | `PROJMUX_CWD` | requested session directory | created session directory | resolved target directory; may be empty if lookup fails | dispatcher working directory |
1183
+ | `PROJMUX_CWD` | requested session directory | created session directory | resolved target directory; may be empty if lookup fails | `PROJMUX_CWD` inherited by the dispatching process; otherwise the nearest `.projmux` or `.git` root above its working directory, else that directory; empty if it cannot be read — see [Send Noti](#send-noti-working-directory) |
876
1184
  | `PROJMUX_SESSION_KIND` | `persistent` or `ephemeral` | `persistent` or `ephemeral` | empty | empty |
877
1185
  | `PROJMUX_VERSION` | projmux version | projmux version | projmux version | projmux version |
878
1186
  | `PROJMUX_SOCKET` | app socket metadata (`projmux`) | app socket metadata (`projmux`) | app socket metadata (`projmux`) | queue-entry socket when known; otherwise omitted |
@@ -889,14 +1197,29 @@ context to either event.
889
1197
 
890
1198
  ## Examples
891
1199
 
1200
+ Save hook scripts outside the legacy paths (`projmux/hooks/<event>`,
1201
+ `.projmux/<event>`, `.projmux/hooks/<event>`). projmux rewrites or ignores
1202
+ files at those paths. Make each script executable and name it in a `run` line.
1203
+ `run` goes through `sh -c`, so `$HOME` expands.
1204
+
892
1205
  ### Global Post Create Stub
893
1206
 
1207
+ Save this as `~/.local/bin/projmux-post-create`:
1208
+
894
1209
  ```bash
895
1210
  #!/usr/bin/env bash
896
1211
  echo "session=$PROJMUX_SESSION cwd=$PROJMUX_CWD kind=$PROJMUX_SESSION_KIND"
897
1212
  tmux -L "$PROJMUX_SOCKET" set-option -p -t "$PROJMUX_PANE" @projmux_initialized 1
898
1213
  ```
899
1214
 
1215
+ Make it executable with `chmod +x ~/.local/bin/projmux-post-create`, then add
1216
+ this to `${XDG_CONFIG_HOME:-$HOME/.config}/projmux/config.toml`:
1217
+
1218
+ ```toml
1219
+ [hooks.post-create]
1220
+ run = "$HOME/.local/bin/projmux-post-create"
1221
+ ```
1222
+
900
1223
  ### Project Startup Command
901
1224
 
902
1225
  ```toml
@@ -906,6 +1229,8 @@ run = "git status --short"
906
1229
 
907
1230
  ### Per Session GH_TOKEN By Repo
908
1231
 
1232
+ Save this as `~/.local/bin/projmux-gh-token`:
1233
+
909
1234
  ```bash
910
1235
  #!/usr/bin/env bash
911
1236
  set -euo pipefail
@@ -919,28 +1244,119 @@ esac
919
1244
  tmux -L "$PROJMUX_SOCKET" set-environment -t "$PROJMUX_SESSION" GH_TOKEN "$token"
920
1245
  ```
921
1246
 
1247
+ Make it executable with `chmod +x ~/.local/bin/projmux-gh-token`, then add this
1248
+ to `${XDG_CONFIG_HOME:-$HOME/.config}/projmux/config.toml`:
1249
+
1250
+ ```toml
1251
+ [hooks.post-create]
1252
+ run = "$HOME/.local/bin/projmux-gh-token"
1253
+ ```
1254
+
922
1255
  `set-environment` only seeds the session env that newly-spawned panes inherit;
923
1256
  it does not retroactively change the current shell. Open new panes via tmux
924
1257
  (`Ctrl-b c`, `Ctrl-b "`, etc.) to pick up the value.
925
1258
 
1259
+ Both examples use `post-create`, and an event has one `run` line. To use both,
1260
+ call them from one line:
1261
+
1262
+ ```toml
1263
+ [hooks.post-create]
1264
+ run = "$HOME/.local/bin/projmux-post-create; $HOME/.local/bin/projmux-gh-token"
1265
+ ```
1266
+
926
1267
  ## Troubleshooting
927
1268
 
928
- - **Nothing happens.** Check the execute bit on the global hook
929
- (`ls -l ~/.config/projmux/hooks/<event>`) or the project hook
930
- (`ls -l .projmux/<event> .projmux/hooks/<event>`). A missing bit makes
931
- projmux skip hook files silently by design. `.projmux/config.toml` does not
932
- need an execute bit.
933
- - **`project hook ... requires trust; skipping in non-interactive context`** or
934
- **`project config ... requires trust; skipping in non-interactive context`.**
1269
+ - **Nothing happens.** Work through these checks in order:
1270
+ 1. Check that a `run` line exists for the event. `projmux hook list` shows
1271
+ the global and project entries. A project hook must be in
1272
+ `.projmux/config.toml` directly in the hook's `PROJMUX_CWD` directory,
1273
+ which is the session directory for `pre-create`, `post-create`, and
1274
+ `post-attach`. For `send-noti` it is the inherited `PROJMUX_CWD`,
1275
+ otherwise the nearest `.projmux` or `.git` root above the notifying
1276
+ process's working directory; see [Send Noti](#send-noti-working-directory).
1277
+ The runner does not look in parent directories. Without
1278
+ `PROJMUX_CWD`, `projmux hook list` uses `.projmux/config.toml` in the
1279
+ current directory, or walks up to the nearest `.projmux` or `.git`. When
1280
+ it shows a parent file, it says that sessions created in the current
1281
+ directory do not run it:
1282
+ `note: sessions created in <dir> do not run this file's pre-create, post-create, or post-attach hooks, [startup], or [env]; only sessions created in <root> do`.
1283
+ `projmux hook validate` prints the same note. `projmux hook trust` and
1284
+ `projmux hook untrust` without `<project>`, and `projmux hook edit
1285
+ <event>` without `--global` (inline or `--editor`), still act on the
1286
+ `<root>` file from such a directory and print the same note after their
1287
+ result line. The note is not printed from `<root>` itself, under
1288
+ `PROJMUX_CWD`, or when you name `<project>` or `--global`. It follows the
1289
+ UI locale; the text above is the `en-US` form. Move the file into the
1290
+ session directory, or create the session in `<root>`.
1291
+ 2. Check for parse errors. If a file cannot be parsed, the whole file is
1292
+ ignored for that run and stderr shows
1293
+ `projmux: <event> hook: global config "<path>" could not be parsed: <reason>`
1294
+ or
1295
+ `projmux: <event> hook: project config ".projmux/config.toml" could not be parsed: <reason>`.
1296
+ `projmux hook validate` prints each file's status, for example
1297
+ `global <path> PARSE ERROR: line 2: value must be a quoted string`,
1298
+ and exits 1.
1299
+ 3. Check that project hooks are on. `PROJMUX_PROJECT_HOOKS=off` or
1300
+ Settings > Labs > Project Hooks turns them off with no warning. Global
1301
+ hooks still run.
1302
+ 4. Check trust. An untrusted or changed project config is skipped; see the
1303
+ trust item below.
1304
+ 5. `post-attach` runs only when projmux switches a client from inside tmux.
1305
+ An attach from outside tmux does not run it.
1306
+ 6. Check for legacy script files. Legacy files are never executed, whatever
1307
+ their execute bit: global
1308
+ `${XDG_CONFIG_HOME:-$HOME/.config}/projmux/hooks/<event>`, and project
1309
+ `.projmux/<event>` and `.projmux/hooks/<event>`. Most projmux commands,
1310
+ such as `projmux hook list`, scan them when they start. Help and
1311
+ read-only commands such as `doctor`, `get`, and `describe` do not:
1312
+ - A one-command script is moved into `[hooks.<event>] run` in the
1313
+ matching `config.toml` and renamed to `<path>.bak`. stderr shows
1314
+ `projmux: migrated legacy <global|project> hook <path> -> [hooks.<event>] run; original kept at <path>.bak`.
1315
+ An existing `run` for that event wins; the script is still renamed.
1316
+ - A multi-line script is left in place and not run. Every scanning
1317
+ command prints
1318
+ `projmux: legacy <global|project> hook <path> has <N> non-trivial lines; declarative migration skipped (multi-line scripts are no longer executed; rewrite manually as run = "bash -c '...'" or run = "./scripts/foo.sh")`.
1319
+ - A symlink is left in place and not run:
1320
+ `projmux: legacy <global|project> hook <path> is a symlink; declarative migration skipped (clean up via the source dotfiles repo)`.
1321
+
1322
+ The project scan runs only when the projmux process has `PROJMUX_CWD` set
1323
+ to the repository, for example
1324
+ `PROJMUX_CWD="$PWD" projmux hook list --project`. Without it, project
1325
+ legacy files are ignored silently. A migrated `.projmux/config.toml` has
1326
+ new content, so it needs trust again.
1327
+ 7. Check the execute bit of the program named in `run`. It needs its own
1328
+ execute bit, or use `run = "bash /path/script"`. Without it, stderr shows
1329
+ `[<event>] sh: 1: <path>: Permission denied` and then
1330
+ `projmux: <event> hook: global config hook: exited with status 126`.
1331
+ `.projmux/config.toml` itself does not need an execute bit.
1332
+ - **`projmux: <event> hook: project config ".projmux/config.toml" requires trust; skipping in non-interactive context`**
1333
+ or
1334
+ **`projmux: <event> hook: project config ".projmux/config.toml" hash changed; trusted sha256=<old> current sha256=<new>; skipping in non-interactive context`.**
935
1335
  Run the same projmux command from an interactive terminal to approve the file,
936
- or set `PROJMUX_PROJECT_HOOKS=off` if project-local execution should be
1336
+ or run `projmux hook trust [<project>]`, which prints `trusted <repo>` and
1337
+ the sha256. Without `<project>` from a subdirectory, it trusts the nearest
1338
+ project root and prints the scope note from check 1 of "Nothing happens".
1339
+ Set `PROJMUX_PROJECT_HOOKS=off` if project-local execution should be
937
1340
  disabled.
938
1341
  - **`projmux: <event> hook: ... timed out after 5s`.** Long-running work
939
1342
  belongs in a backgrounded child (`(slow-thing &) >/dev/null 2>&1`). The hook
940
- itself must return within 5s or projmux kills it.
941
- - **`projmux: <event> hook: hook ... exited with status N`.** The script
942
- returned non-zero. For `pre-create`, creation aborts; for other events,
943
- projmux logs once and moves on.
944
- - **Lines appear with `[post-create] `, `[pre-create] `, or `[post-attach] `
945
- prefixes.** Expected; hook stdout/stderr are multiplexed into projmux's
946
- stderr stream.
1343
+ itself must return within 5s or projmux kills it. projmux runs the hook in
1344
+ its own process group and on timeout sends SIGKILL to that whole group, so
1345
+ children and grandchildren started by the hook stop too; if projmux itself
1346
+ receives SIGINT, SIGTERM, or SIGHUP while a hook runs, it kills the hook's
1347
+ group the same way before exiting. A process that left the group or session
1348
+ (for example via `setsid` or by daemonizing) is not killed, and SIGKILL gives
1349
+ no chance to clean up, so residue such as a stale `.git/index.lock` can
1350
+ remain; remove it by hand. Because the hook runs outside the terminal's
1351
+ foreground process group, a hook that reads `/dev/tty` directly is stopped
1352
+ (SIGTTIN) until the timeout. For `pre-create`, creation stops and the command
1353
+ fails with `pre-create hook for tmux session "<name>": timed out after 5s`.
1354
+ - **`projmux: <event> hook: ... exited with status N`.** The hook returned
1355
+ non-zero. projmux logs once and moves on. A global hook shows
1356
+ `global config hook: exited with status N`; a project hook shows
1357
+ `project config ".projmux/config.toml": exited with status N`. For
1358
+ `pre-create`, this warning is not logged. Creation stops and the command
1359
+ fails with `pre-create hook for tmux session "<name>": exited with status N`.
1360
+ - **Lines appear with `[post-create] `, `[pre-create] `, `[post-attach] `, or
1361
+ `[send-noti] ` prefixes.** Expected; hook stdout/stderr are multiplexed into
1362
+ projmux's stderr stream.