projmux 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/hooks.md CHANGED
@@ -15,8 +15,8 @@ projmux-owned internal tmux hooks such as `pane-focus-in`, `pane-focus-out`,
15
15
  | --- | --- | --- | --- |
16
16
  | `pre-create` | Before projmux creates a missing persistent or ephemeral session | Non-zero exit, exec error, or timeout aborts creation | Logged with `[pre-create] ` |
17
17
  | `post-create` | After projmux creates a brand-new persistent or ephemeral session | Logged and ignored; creation continues | Logged with `[post-create] ` |
18
- | `pane-startup` | After `post-create`, once the initial pane of a brand-new session reaches a shell prompt | Logged and ignored; empty output is no-op | Captured as the command to send into the pane |
19
18
  | `post-attach` | After projmux switches the current tmux client to an existing session/target from inside tmux | Logged and ignored | Logged with `[post-attach] ` |
19
+ | `send-noti` | After `projmux notify push` (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] ` |
20
20
 
21
21
  Deferred Phase A candidates remain future work until their behavior can be
22
22
  specified without exposing projmux's internal tmux hook machinery: pane exit,
@@ -24,47 +24,24 @@ window create/rename, and focus-change hook events.
24
24
 
25
25
  ## Where Hooks Live
26
26
 
27
- Global hooks live under the XDG config directory:
27
+ Global hooks live in the XDG config file:
28
28
 
29
29
  ```text
30
- ${XDG_CONFIG_HOME:-$HOME/.config}/projmux/hooks/<event>
31
- ```
32
-
33
- For example:
34
-
35
- ```text
36
- ${XDG_CONFIG_HOME:-$HOME/.config}/projmux/hooks/post-create
37
- ${XDG_CONFIG_HOME:-$HOME/.config}/projmux/hooks/pane-startup
30
+ ${XDG_CONFIG_HOME:-$HOME/.config}/projmux/config.toml
38
31
  ```
39
32
 
40
33
  Project-local hooks are discovered from the lifecycle context's `PROJMUX_CWD`:
41
34
 
42
35
  ```text
43
- <repo>/.projmux/<event>
44
- <repo>/.projmux/hooks/<event>
45
36
  <repo>/.projmux/config.toml
46
37
  ```
47
38
 
48
- For example, `pane-startup` discovery checks:
49
-
50
- ```text
51
- <repo>/.projmux/pane-startup
52
- <repo>/.projmux/hooks/pane-startup
53
- ```
54
-
55
- Each hook file must exist, be a regular file or symlink (not a directory), and
56
- have the owner-execute bit set. Anything else is silently skipped.
57
-
58
- For each event, projmux runs at most one project-local file: first
59
- `.projmux/<event>` if executable, otherwise `.projmux/hooks/<event>` if
60
- executable. Discovery does not walk parent directories and does not run hooks
61
- from status, preview, or picker hot paths.
39
+ File-form hooks from the historical `.projmux/<event>` and
40
+ `.projmux/hooks/<event>` layouts are no longer executed. Use declarative
41
+ `[hooks.<event>] run` entries instead.
62
42
 
63
43
  If both a global hook and a project-local hook exist for an event, projmux runs
64
- the global hook first, then the project-local hook, then any matching
65
- declarative config command from `.projmux/config.toml`. For `pane-startup`, the
66
- last non-empty trimmed stdout wins. Config `pane-startup` commands therefore
67
- override file hooks deterministically.
44
+ the global hook first, then the project-local hook.
68
45
 
69
46
  ## Project Config
70
47
 
@@ -81,12 +58,12 @@ run = "echo checking"
81
58
  [hooks.post-create]
82
59
  run = "echo created $PROJMUX_SESSION"
83
60
 
84
- [hooks.pane-startup]
85
- run = "echo make test"
86
-
87
61
  [hooks.post-attach]
88
62
  run = "echo attached"
89
63
 
64
+ [hooks.send-noti]
65
+ run = "jq -r '.message' | xargs -I{} send-slack \"{}\""
66
+
90
67
  [env]
91
68
  FOO = "bar"
92
69
 
@@ -97,18 +74,22 @@ namespace = "tools"
97
74
 
98
75
  Only quoted string values are supported. Unknown sections or keys make the
99
76
  config file invalid for this run. Supported hook events are the public lifecycle
100
- events listed above: `pre-create`, `post-create`, `pane-startup`, and
101
- `post-attach`. Internal tmux hook names such as `after-select-pane` are rejected.
102
- Phase C secret interpolation is not implemented; values are used literally.
77
+ events listed above: `pre-create`, `post-create`, `post-attach`, and
78
+ `send-noti`. Internal tmux hook names such as
79
+ `after-select-pane` are rejected. Phase C secret interpolation is not
80
+ implemented; values are used literally.
103
81
 
104
82
  `[hooks.<event>] run` executes through `sh -c` with the same timeout, logging,
105
- environment, and failure model as file hooks. For `pane-startup`, stdout is
106
- captured as the pane command.
83
+ environment, and failure model for all hook events.
84
+
85
+ `[hooks.send-noti] run` is the declarative forward path for queue-backed
86
+ notifications. It does not replace desktop notifications or the in-app queue;
87
+ it runs in parallel after the queue write succeeds so users can fan out to
88
+ Slack, webhooks, or custom scripts without changing the built-in notification
89
+ path.
107
90
 
108
91
  `[startup] run` is a direct shorthand for a startup pane command. It is not
109
92
  executed as shell by the hook runner; the string itself is sent to the new pane.
110
- When both `[hooks.pane-startup] run` and `[startup] run` are present,
111
- `[startup] run` wins because it is applied last.
112
93
 
113
94
  `[env]` values are added to hook process environments and to newly-created tmux
114
95
  session environments. For new sessions, projmux passes them to
@@ -128,17 +109,17 @@ values.
128
109
 
129
110
  Global hooks under `$XDG_CONFIG_HOME` are prompt-free.
130
111
 
131
- Project-local hooks and `.projmux/config.toml` are gated by trust-on-first-use
132
- for every user-facing hook event in this file. A repository hook file or config
133
- file must be approved before projmux runs or applies it. Approving "always"
112
+ Project-local `.projmux/config.toml` files are gated by trust-on-first-use
113
+ before projmux runs hooks or applies startup/session environment settings.
114
+ Approving "always"
134
115
  records the file content hash in:
135
116
 
136
117
  ```text
137
118
  ${XDG_STATE_HOME:-$HOME/.local/state}/projmux/trusted-projects.json
138
119
  ```
139
120
 
140
- The trust key is the absolute repository path and each executable hook or config
141
- path is stored relative to that repository. Each project entry has a
121
+ The trust key is the absolute repository path and the config path is stored
122
+ relative to that repository. Each project entry has a
142
123
  `trusted_at` timestamp and a `files` map of relative paths to SHA-256 hashes.
143
124
  When file content changes, projmux asks again and shows the old and new SHA-256
144
125
  hashes. In non-interactive contexts such as tmux run-shell or CI, untrusted or
@@ -148,27 +129,462 @@ Set `PROJMUX_PROJECT_HOOKS=off` to disable project-local hook discovery
148
129
  entirely. Project-local hooks can also be disabled from `projmux settings`
149
130
  under Labs. The global hook still runs either way.
150
131
 
151
- ## Pane Startup
132
+ ## Startup Commands
152
133
 
153
- `pane-startup` runs only for the initial pane created with a new
134
+ `[startup] run` applies only to the initial pane created with a new
154
135
  projmux-managed session. It runs after `post-create` so `post-create` can seed
155
136
  session-level tmux environment before the startup command is sent. It does not
156
137
  run when projmux attaches to an existing session or target.
157
138
 
158
- Before running `pane-startup`, projmux polls tmux `pane_current_command` for the
159
- new pane and waits until it reports a shell command. This avoids fixed sleeps as
160
- the primary readiness mechanism.
139
+ Before sending the startup command, projmux polls tmux `pane_current_command`
140
+ for the new pane and waits until it reports a shell command. This avoids fixed
141
+ sleeps as the primary readiness mechanism.
161
142
 
162
- The hook's trimmed stdout is treated as the command to send into the new pane:
143
+ The configured string is sent into the new pane:
163
144
 
164
145
  ```text
165
146
  tmux send-keys -t <pane-id> <command> Enter
166
147
  ```
167
148
 
168
- Empty stdout is a no-op. Hook stderr is still forwarded to projmux's stderr with
169
- the `[pane-startup] ` prefix. If both global and project-local hooks emit a
170
- command, the project-local command wins because project hooks run after global
171
- hooks.
149
+ An empty `[startup] run` is a no-op.
150
+
151
+ ## Send Noti
152
+
153
+ `send-noti` fires only after the notify queue write succeeds. The queue entry is
154
+ already durable before the hook starts, so a failing or slow hook cannot drop
155
+ the notification or block the normal desktop notification flow.
156
+
157
+ The hook runs asynchronously with the same default timeout (`5s`) as other
158
+ hooks. projmux does not wait for completion before returning from
159
+ `projmux notify push`.
160
+
161
+ stdin receives one JSON object:
162
+
163
+ ```json
164
+ {
165
+ "event": "send-noti",
166
+ "id": "ai:main:%9",
167
+ "type": "ai-reply-ready",
168
+ "agent": "claude",
169
+ "topic": "worker loop",
170
+ "pane": "%9",
171
+ "session": "main",
172
+ "message": "claude: reply ready · worker loop",
173
+ "metadata": {
174
+ "agent": "claude"
175
+ },
176
+ "created_at": "2026-05-12T02:03:04Z"
177
+ }
178
+ ```
179
+
180
+ The hook environment also includes:
181
+
182
+ | Variable | Description |
183
+ | --- | --- |
184
+ | `PROJMUX_NOTIFY_ID` | Queue entry id |
185
+ | `PROJMUX_NOTIFY_TYPE` | Notification kind (`ai-reply-ready`, `external`, etc.) |
186
+ | `PROJMUX_NOTIFY_AGENT` | AI agent label when known |
187
+ | `PROJMUX_NOTIFY_TOPIC` | AI topic when known |
188
+ | `PROJMUX_NOTIFY_PANE` | Target pane id when known |
189
+ | `PROJMUX_NOTIFY_SESSION` | Target tmux session |
190
+ | `PROJMUX_NOTIFY_MESSAGE` | Human-readable message text |
191
+ | `PROJMUX_NOTIFY_HOOK_DEPTH` | Recursion guard; values `>= 1` suppress nested `send-noti` dispatch |
192
+
193
+ If a `send-noti` hook itself calls `projmux notify push`, projmux sees
194
+ `PROJMUX_NOTIFY_HOOK_DEPTH=1` in the child environment and skips another
195
+ `send-noti` hook fire. The queue write itself still succeeds.
196
+
197
+ `Settings > Notifications > Delivery sources` surfaces the active Codex hooks,
198
+ Claude, and tmux AI notify diagnostics: status, conflicts, config paths, and
199
+ copyable CLI install/remove/dry-run commands. It does not install or remove
200
+ external Codex, Claude, or tmux settings.
201
+
202
+ ## Codex Hooks Engine
203
+
204
+ `projmux doctor` reports Codex hooks-engine wiring separately from legacy
205
+ notify, including unmanaged hooks/conflict details and the relevant
206
+ `projmux ai integrate codex` commands.
207
+
208
+ `projmux ai ingest codex-hook` is the conservative core ingest path for Codex
209
+ hooks-engine events. It reads a single JSON payload from stdin. The embedded
210
+ default install catalog is based on Codex CLI 0.130.0:
211
+
212
+ | Event | Behavior |
213
+ | --- | --- |
214
+ | `PreToolUse` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
215
+ | `UserPromptSubmit` | marks the matched pane hook-active and sets AI state to thinking/busy; no notify queue entry is pushed |
216
+ | `PermissionRequest` | pushes a critical approval row with the tool name and a concise tool/action summary |
217
+ | `PostToolUse` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
218
+ | `PreCompact` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
219
+ | `PostCompact` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
220
+ | `SessionStart` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
221
+ | `Stop` | pushes an info Codex completion row |
222
+
223
+ Codex hook payload parsing accepts the common fields
224
+ `hook_event_name`/`event_name`, `thread_id`, `session_id`, `turn_id`, `cwd`,
225
+ `transcript_path`, `model`, `tool_name`, nested `tool.name`, `tool_input`, and
226
+ `input`. For pane matching, Codex hook ingest uses `thread_id` when available
227
+ and falls back to treating `session_id` as the thread identity so existing
228
+ `matchAIPane` matching can reuse cached pane metadata.
229
+
230
+ `projmux ai integrate codex` manages a separate
231
+ `~/.codex/config.toml` marker block for the hooks engine. If a `[features]`
232
+ table already exists, projmux merges `hooks = true` into that table instead of
233
+ creating a duplicate table. Older projmux-managed `codex_hooks = true` entries
234
+ are migrated to `hooks = true` to avoid Codex's deprecation warning:
235
+
236
+ ```toml
237
+ [features]
238
+ hooks = true
239
+
240
+ [[hooks.PreToolUse]]
241
+ matcher = "*"
242
+ [[hooks.PreToolUse.hooks]]
243
+ type = "command"
244
+ command = "projmux ai ingest codex-hook >/dev/null 2>&1 || true"
245
+
246
+ [[hooks.PermissionRequest]]
247
+ matcher = "*"
248
+ [[hooks.PermissionRequest.hooks]]
249
+ type = "command"
250
+ command = "projmux ai ingest codex-hook >/dev/null 2>&1 || true"
251
+
252
+ [[hooks.PostToolUse]]
253
+ matcher = "*"
254
+ [[hooks.PostToolUse.hooks]]
255
+ type = "command"
256
+ command = "projmux ai ingest codex-hook >/dev/null 2>&1 || true"
257
+
258
+ [[hooks.PreCompact]]
259
+ matcher = "*"
260
+ [[hooks.PreCompact.hooks]]
261
+ type = "command"
262
+ command = "projmux ai ingest codex-hook >/dev/null 2>&1 || true"
263
+
264
+ [[hooks.PostCompact]]
265
+ matcher = "*"
266
+ [[hooks.PostCompact.hooks]]
267
+ type = "command"
268
+ command = "projmux ai ingest codex-hook >/dev/null 2>&1 || true"
269
+
270
+ [[hooks.SessionStart]]
271
+ matcher = "*"
272
+ [[hooks.SessionStart.hooks]]
273
+ type = "command"
274
+ command = "projmux ai ingest codex-hook >/dev/null 2>&1 || true"
275
+
276
+ [[hooks.UserPromptSubmit]]
277
+ matcher = "*"
278
+ [[hooks.UserPromptSubmit.hooks]]
279
+ type = "command"
280
+ command = "projmux ai ingest codex-hook >/dev/null 2>&1 || true"
281
+
282
+ [[hooks.Stop]]
283
+ matcher = "*"
284
+ [[hooks.Stop.hooks]]
285
+ type = "command"
286
+ command = "projmux ai ingest codex-hook >/dev/null 2>&1 || true"
287
+ ```
288
+
289
+ Repeated installs are idempotent and preserve unrelated Codex config, including
290
+ unmanaged hook entries for the same events. If projmux sees an unmanaged
291
+ `projmux ai ingest codex-hook` command, it refuses to install over it rather
292
+ than guessing ownership. `--dry-run` previews the TOML update. `--remove`
293
+ removes projmux-managed Codex hooks wiring.
294
+
295
+ The Codex hooks install list is catalog-driven. Projmux ships an embedded
296
+ default catalog at `internal/app/ai_hook_catalogs/codex.json`, and merges an
297
+ optional local override from:
298
+
299
+ ```text
300
+ ${XDG_CONFIG_HOME:-$HOME/.config}/projmux/ai-hooks.d/codex.json
301
+ ```
302
+
303
+ Override events replace matching embedded events by name and append new names,
304
+ so a local file can disable a stale event with `"install": false` or add a new
305
+ Codex event before projmux itself is released. Supported `action` values are
306
+ `notify`, `state`, and `quiet`; install uses only events whose effective
307
+ `install` value is true. Removal remains marker-block based rather than catalog
308
+ based, so older projmux-managed Codex hook blocks are still removed after the
309
+ catalog changes.
310
+
311
+ ```json
312
+ {
313
+ "provider": "codex",
314
+ "events": [
315
+ { "name": "Stop", "install": false, "action": "notify" },
316
+ { "name": "FutureEvent", "install": true, "action": "quiet" }
317
+ ]
318
+ }
319
+ ```
320
+
321
+ Codex ingest has built-in notify/state handlers for the known reply-ready,
322
+ approval, and prompt-submit events. Events without a specialized handler,
323
+ including override-added events, are quiet/log-only after pane matching; a
324
+ catalog `"action": "quiet"` makes that quiet fallback explicit. Catalog
325
+ `"notify"` and `"state"` entries still need a built-in handler before they can
326
+ push queue rows or change pane state.
327
+
328
+ Codex may require reviewing or trusting hooks through its `/hooks` flow before
329
+ commands run. Projmux only writes the managed config block; it does not attempt
330
+ to auto-trust hooks.
331
+
332
+ ## Tmux Bell Fallback
333
+
334
+ `projmux doctor` reports whether the current tmux server has a
335
+ projmux-managed bell hook installed. The diagnostic is read-only; it only
336
+ inspects current `alert-bell` hooks and prints the matching integrate commands.
337
+
338
+ `projmux ai integrate tmux-bell` is the opt-in fallback for AI CLIs that do
339
+ not expose structured hooks but do emit BEL or OSC 9. It mutates the current
340
+ tmux server only; it does not edit tmux config files. Install applies these
341
+ server settings and appends a marked `alert-bell` hook:
342
+
343
+ ```text
344
+ tmux set-option -g allow-passthrough on
345
+ tmux set-option -g monitor-bell on
346
+ tmux set-option -g bell-action other
347
+ tmux set-hook -ag alert-bell run-shell -b 'projmux ai ingest bell --pane "#{pane_id}" >/dev/null 2>&1 || true # projmux-managed:tmux-bell:v1'
348
+ ```
349
+
350
+ The hook calls `projmux ai ingest bell --pane <pane_id>`. Bell ingest resolves
351
+ the target pane through tmux and pushes an info notify queue row such as
352
+ `bell · Claude CLI`, with metadata including `agent=bell`, `event=bell`,
353
+ session/window/pane, pane title, command, and socket path when available. The
354
+ pane does not need `@projmux_ai_agent` or any other AI-managed option because
355
+ this path is for unknown tools. tmux `alert-bell` is a window alert; tmux 3.4
356
+ does not expose `#{hook_pane}` there, so projmux uses `#{pane_id}` as the best
357
+ available pane context.
358
+
359
+ Repeated bells from the same pane are suppressed for 5 seconds using a
360
+ pane-local tmux timestamp option. The notify row also uses a stable
361
+ `ai:bell:<session>:<pane>` id, so later non-suppressed bells refresh the same
362
+ queue entry rather than creating unbounded duplicates.
363
+
364
+ `--dry-run` prints the tmux commands without applying them. `--remove` reads
365
+ the current `alert-bell` hooks and unsets only entries carrying
366
+ `projmux-managed:tmux-bell:v1`, preserving unmanaged user hooks.
367
+
368
+ ## Claude Code Hook Ingest
369
+
370
+ `projmux doctor` reports Claude Code hook wiring in `~/.claude/settings.json`,
371
+ including unmanaged projmux ingest command conflicts and the relevant
372
+ `projmux ai integrate claude` commands.
373
+
374
+ `projmux ai ingest claude-hook` is the conservative core ingest path for
375
+ Claude Code hooks. It reads a single JSON payload from stdin. The embedded
376
+ default install catalog is based on Claude Code 2.1.140 and represents the
377
+ 29 hook events visible in that version:
378
+
379
+ | Event | Behavior |
380
+ | --- | --- |
381
+ | `PreToolUse` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
382
+ | `PostToolUse` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
383
+ | `PostToolUseFailure` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
384
+ | `PostToolBatch` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
385
+ | `PermissionDenied` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
386
+ | `Notification` | pushes a Claude notify row for response-ready, approval-required, or input-ready based on `notification_type` |
387
+ | `UserPromptSubmit` | marks the matched pane hook-active and sets AI state to thinking/busy; no notify queue entry is pushed |
388
+ | `UserPromptExpansion` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
389
+ | `SessionStart` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
390
+ | `Stop` | pushes a Claude completion row, using the last assistant transcript text when `transcript_path` is readable |
391
+ | `StopFailure` | pushes a critical Claude error row with error type/message metadata when present |
392
+ | `SubagentStart` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
393
+ | `SubagentStop` | marks the pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
394
+ | `PreCompact` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
395
+ | `PostCompact` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
396
+ | `SessionEnd` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
397
+ | `PermissionRequest` | pushes a critical approval row with the tool name and a concise tool input summary |
398
+ | `Setup` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
399
+ | `TeammateIdle` | pushes an info Claude teammate waiting row with teammate context metadata when present |
400
+ | `TaskCreated` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
401
+ | `TaskCompleted` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
402
+ | `Elicitation` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
403
+ | `ElicitationResult` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
404
+ | `ConfigChange` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
405
+ | `InstructionsLoaded` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
406
+ | `WorktreeCreate` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
407
+ | `WorktreeRemove` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
408
+ | `CwdChanged` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
409
+ | `FileChanged` | marks the matched pane hook-active and writes a quiet ingest diagnostic; no notify queue entry is pushed |
410
+
411
+ Hook-generated queue rows use the same compact body catalog: agent label,
412
+ event category, then the best available summary (Codex assistant text, Claude
413
+ tool/action summary, transcript summary, error, or teammate labels). Structured
414
+ payload details remain in `metadata`, which is passed through the `send-noti`
415
+ JSON payload and `notify list --json`; the sidebar stays compact and does not
416
+ expand a separate metadata detail view. `SubagentStop` remains parsed for
417
+ diagnostics but is intentionally excluded from hook notifications because it can
418
+ fire at high volume.
419
+
420
+ Pane matching follows the shared AI ingest order: inherited `$TMUX_PANE`, then
421
+ payload `cwd`, then cached session id pane options. A matched pane is marked
422
+ with `@projmux_ai_hook_active=1`, so `projmux ai watch-title` skips the pane
423
+ and hook payloads become the primary signal.
424
+
425
+ The Claude hook payload is intentionally accepted directly at the ingest
426
+ boundary. Core identity fields accept `hook_event_name`/`event_name`,
427
+ `session_id`/`session-id`, `cwd`/`workspace`/`project_dir`, and nested
428
+ `workspace.cwd|path`. Extra-event fields are intentionally tolerant while the
429
+ upstream schemas settle:
430
+
431
+ | Event | Accepted fields |
432
+ | --- | --- |
433
+ | `StopFailure` | `error_type`, `errorType`, `failure_type`, `failureType`; `error_message`, `errorMessage`, `message`, `reason`; nested `error.type`, `error.name`, `error.code`, `error.message`, `error.text`, `error.reason` |
434
+ | `SubagentStop` | `subagent_type`, `subagentType`, `agent_type`, `agentType`; `subagent_id`, `subagentId`, `agent_id`, `agentId`; nested `subagent.type`, `subagent.name`, `subagent.kind`, `subagent.id`, `subagent.subagent_id`, `subagent.agent_id` |
435
+ | `TeammateIdle` | `teammate_name`, `teammateName`, `teammate`; `teammate_id`, `teammateId`; `teammate_context`, `teammateContext`, `context`, `reason`, `message`; nested `teammate.name`, `teammate.type`, `teammate.kind`, `teammate.id`, `teammate.teammate_id`, `teammate.context`, `teammate.status`, `teammate.reason`, `teammate.message` |
436
+
437
+ `projmux ai integrate claude` manages user-level Claude Code hook settings in
438
+ `~/.claude/settings.json`. Claude Code hooks are configured under the top-level
439
+ `hooks` object, with each event containing matcher entries and each matcher
440
+ entry containing a `hooks` array. Projmux omits `matcher`, which Claude Code
441
+ treats as "match all" for the event; this keeps `Notification` notification
442
+ types and `PermissionRequest` tool names broad while this integration remains
443
+ an observability hook:
444
+
445
+ ```json
446
+ {
447
+ "hooks": {
448
+ "Notification": [
449
+ {
450
+ "hooks": [
451
+ {
452
+ "type": "command",
453
+ "command": "projmux ai ingest claude-hook >/dev/null 2>&1 || true # projmux-managed:claude-hook:v1"
454
+ }
455
+ ]
456
+ }
457
+ ],
458
+ "Stop": [
459
+ {
460
+ "hooks": [
461
+ {
462
+ "type": "command",
463
+ "command": "projmux ai ingest claude-hook >/dev/null 2>&1 || true # projmux-managed:claude-hook:v1"
464
+ }
465
+ ]
466
+ }
467
+ ],
468
+ "UserPromptSubmit": [
469
+ {
470
+ "hooks": [
471
+ {
472
+ "type": "command",
473
+ "command": "projmux ai ingest claude-hook >/dev/null 2>&1 || true # projmux-managed:claude-hook:v1"
474
+ }
475
+ ]
476
+ }
477
+ ],
478
+ "PermissionRequest": [
479
+ {
480
+ "hooks": [
481
+ {
482
+ "type": "command",
483
+ "command": "projmux ai ingest claude-hook >/dev/null 2>&1 || true # projmux-managed:claude-hook:v1"
484
+ }
485
+ ]
486
+ }
487
+ ],
488
+ "StopFailure": [
489
+ {
490
+ "hooks": [
491
+ {
492
+ "type": "command",
493
+ "command": "projmux ai ingest claude-hook >/dev/null 2>&1 || true # projmux-managed:claude-hook:v1"
494
+ }
495
+ ]
496
+ }
497
+ ],
498
+ "SubagentStop": [
499
+ {
500
+ "hooks": [
501
+ {
502
+ "type": "command",
503
+ "command": "projmux ai ingest claude-hook >/dev/null 2>&1 || true # projmux-managed:claude-hook:v1"
504
+ }
505
+ ]
506
+ }
507
+ ],
508
+ "TeammateIdle": [
509
+ {
510
+ "hooks": [
511
+ {
512
+ "type": "command",
513
+ "command": "projmux ai ingest claude-hook >/dev/null 2>&1 || true # projmux-managed:claude-hook:v1"
514
+ }
515
+ ]
516
+ }
517
+ ]
518
+ }
519
+ }
520
+ ```
521
+
522
+ The marker lives inside the command string because JSON settings cannot carry
523
+ comments. Repeated installs are idempotent: projmux removes its marked commands
524
+ and re-adds the current managed command while preserving unrelated settings and
525
+ hooks. `--remove` deletes only marked commands, and `--dry-run` previews the
526
+ JSON without writing. Removal and unmanaged conflict detection scan every event
527
+ under the settings `hooks` object, not just the current catalog. If any event
528
+ already has an unmanaged command that invokes `projmux ai ingest claude-hook`,
529
+ projmux refuses to install over it because it cannot tell whether the command
530
+ is user-owned or stale projmux wiring.
531
+
532
+ The Claude hooks install list is catalog-driven. Projmux ships an embedded
533
+ default catalog at `internal/app/ai_hook_catalogs/claude.json`, and merges an
534
+ optional local override from:
535
+
536
+ ```text
537
+ ${XDG_CONFIG_HOME:-$HOME/.config}/projmux/ai-hooks.d/claude.json
538
+ ```
539
+
540
+ Override events replace matching embedded events by name and append new names,
541
+ so a local file can disable a stale event with `"install": false` or add a new
542
+ Claude Code event before projmux itself is released. Supported `action` values
543
+ are `notify`, `state`, and `quiet`; install uses only events whose effective
544
+ `install` value is true. Removal and unmanaged conflict detection scan every
545
+ event under `~/.claude/settings.json` for the projmux command marker rather
546
+ than trusting the current catalog, so stale managed events from older catalogs
547
+ are still removed safely. `SubagentStop` is wired for hook-active pane marking
548
+ and ingest debugging only.
549
+
550
+ ```json
551
+ {
552
+ "provider": "claude",
553
+ "events": [
554
+ { "name": "Stop", "install": false, "action": "notify" },
555
+ { "name": "FutureEvent", "install": true, "action": "quiet" }
556
+ ]
557
+ }
558
+ ```
559
+
560
+ Claude ingest has built-in notify/state handlers for the known completion,
561
+ notification, approval, prompt-submit, error, and teammate-idle events. Events
562
+ without a specialized handler, including unknown future events, are
563
+ quiet/log-only after pane matching. Catalog `"action": "quiet"` is used for
564
+ that fallback; catalog `"notify"` and `"state"` entries still need built-in
565
+ handler code for event-specific queue rows or state transitions.
566
+
567
+ ## Ingest Debug Log
568
+
569
+ Every `projmux ai ingest ...` path appends compact JSONL diagnostics to
570
+ `$XDG_STATE_HOME/projmux/ai-ingest.log`, or
571
+ `~/.local/state/projmux/ai-ingest.log` when `XDG_STATE_HOME` is unset. The log
572
+ records source, event, result, pane, match identifiers, and a short reason for
573
+ parse errors, unsupported events, missing pane matches, deduped bells,
574
+ state-only transitions, quiet high-volume events, and notify pushes. Raw hook
575
+ payloads are not stored.
576
+
577
+ Use `projmux ai ingest log` to inspect recent entries:
578
+
579
+ ```text
580
+ projmux ai ingest log --tail 20
581
+ projmux ai ingest log --json --tail 20
582
+ projmux ai ingest log --path
583
+ ```
584
+
585
+ The file is capped at 1 MiB. When an append grows it past the cap, projmux
586
+ keeps the most recent roughly 512 KiB and trims from the next JSONL boundary so
587
+ the remaining file still starts at a whole entry.
172
588
 
173
589
  ## Pre Create Abort
174
590
 
@@ -207,15 +623,11 @@ Hooks inherit projmux's environment, plus:
207
623
  echo "session=$PROJMUX_SESSION cwd=$PROJMUX_CWD kind=$PROJMUX_SESSION_KIND"
208
624
  ```
209
625
 
210
- ### Project Pane Startup Command
626
+ ### Project Startup Command
211
627
 
212
- ```bash
213
- mkdir -p .projmux
214
- cat > .projmux/pane-startup <<'EOF'
215
- #!/usr/bin/env bash
216
- echo "git status --short"
217
- EOF
218
- chmod +x .projmux/pane-startup
628
+ ```toml
629
+ [startup]
630
+ run = "git status --short"
219
631
  ```
220
632
 
221
633
  ### Per Session GH_TOKEN By Repo
@@ -257,4 +669,4 @@ it does not retroactively change the current shell. Open new panes via tmux
257
669
  projmux logs once and moves on.
258
670
  - **Lines appear with `[post-create] `, `[pre-create] `, or `[post-attach] `
259
671
  prefixes.** Expected; hook stdout/stderr are multiplexed into projmux's
260
- stderr stream. `pane-startup` stdout is captured as the pane command instead.
672
+ stderr stream.
package/docs/install.md CHANGED
@@ -33,6 +33,12 @@ when available. If an installer-supported update is available, pressing Enter
33
33
  at the inline prompt runs `projmux update apply`; answering `n` prints that
34
34
  manual command and continues into the shell.
35
35
 
36
+ After the shell tmux client attaches, projmux consumes the pending welcome
37
+ marker once and shows the same guide in a dismissable popup. If you want to
38
+ revisit that guide later, run `projmux welcome`. Set `PROJMUX_WELCOME=off`
39
+ before launching `projmux shell` to suppress the automatic attach popup without
40
+ disabling the manual command.
41
+
36
42
  ## Runtime Tools
37
43
 
38
44
  Normal use needs: