1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
//! `tm hook` `PreToolUse` Bash command-rewrite logic — Option 0 spike (#1956).
//!
//! Why: `docs/specs/tool-output-interception-seam.md` §Option 0 documents a
//! pre-context-insertion compression seam for Bash: rewrite the *command*
//! Claude Code is about to execute so the subprocess's own stdout is already
//! filtered by the time the tool result is captured — the same trick
//! claude-mpm's `ztk` hook uses (`hooks/ztk_hook.py`), but piping through
//! `tm compress` instead of an external Zig binary. That doc's Tradeoffs
//! section calls out the exact incident this must defend against from day
//! one: an earlier orchestrator command (a SAM build) broke when wrapped
//! unconditionally, because appending a pipe can change exit-code semantics
//! (`set -o pipefail` interactions) for build tools that spawn long
//! subprocess chains.
//! What: [`rewrite_bash_command_for_compression`] is the single entry point —
//! it returns `None` (meaning: do not rewrite, forward the command
//! unmodified) for empty commands, day-one orchestrator exclusions
//! (`make`/`sam`/`rake`/`gradle`/`gradlew`/`mvn`/`ant`/`cdk`/`terraform`,
//! mirroring claude-mpm's `_ORCHESTRATOR_EXCLUSIONS`), any command that
//! already contains pipe/chain/redirection/background composition (`|`,
//! `&`, `;`, `>`, `<`) since appending another pipe to those is the same class of
//! risk (redirection in particular: piping `tm compress` after an
//! already-redirected stdout gets it empty input for no benefit — a
//! trusty-review finding, PR #1968), and any command whose derived tool
//! name would embed shell metacharacters (see
//! [`is_safe_tool_name`]). Otherwise it returns `Some(rewritten)` with
//! `| tm compress --tool "<effective tool name>"` appended — see
//! [`effective_tool_name`] for why the tool value is derived from the
//! command rather than a hardcoded `"bash"`.
//! [`build_pretooluse_rewrite_response`] builds the
//! `hookSpecificOutput.updatedInput` JSON body `tm hook` prints to stdout so
//! Claude Code substitutes the rewritten command before executing it — this
//! exact shape is confirmed against the live Claude Code hooks reference
//! (<https://code.claude.com/docs/en/hooks>, confirmed 2026-07-03; see that
//! function's doc comment for the citation).
//! Test: `rewrite_*`, `is_orchestrator_command_*`, `first_command_token_*`,
//! `is_safe_tool_name_*`, `build_pretooluse_rewrite_response_*` below.
/// Day-one orchestrator-command exclusion list.
///
/// Why: These tools spawn long, multi-stage subprocess chains where an
/// unconditional `| tm compress --tool bash` wrap has already caused a real
/// incident (a SAM build silently truncated at a later stage — see the
/// design doc's Tradeoffs section and `docs/specs/SPEC-INSTALLER-01.md:150`).
/// Mirrors claude-mpm's `_ORCHESTRATOR_EXCLUSIONS` (`hooks/ztk_hook.py`
/// ~lines 49-69) so this spike inherits the same day-one safety margin
/// rather than rediscovering the failure mode.
/// What: Matched against the command's first whitespace-delimited token
/// (after stripping env-var-assignment prefixes, `sudo`/`env`, and any
/// leading path) by [`is_orchestrator_command`].
/// Test: `is_orchestrator_command_matches_known_exclusions`.
const ORCHESTRATOR_EXCLUSIONS: & = &;
/// Decide whether `command` is safe to rewrite, and build the rewrite.
///
/// Why: Centralises the "is this rewrite safe?" decision so `tm hook` can
/// stay a thin caller — better to under-rewrite (skip a compressible
/// command) than to break a working one, per the design doc's explicit
/// tradeoff framing.
/// What: Returns `None` (no-op — forward the original command unmodified)
/// when `command` is empty/whitespace-only, matches
/// [`is_orchestrator_command`], already contains pipe/chain composition
/// (see [`has_unsafe_pipe_composition`]), or derives a tool name containing
/// shell metacharacters (see [`is_safe_tool_name`] — a
/// trusty-review-flagged injection risk, PR #1968: the derived name is
/// embedded verbatim inside a double-quoted shell argument below, and
/// double quotes do not stop POSIX command substitution). Otherwise returns
/// `Some("<command> | tm compress --tool \"<effective tool name>\"")` —
/// the `--tool` value comes from [`effective_tool_name`], **not** a
/// hardcoded `"bash"`: `compress_tool_output`'s dispatch table
/// (`trusty-agents-common::compress::tool_output`) matches filters by
/// substring against the tool name (`"cargo"`, `"diff"`, `"log"`, …), so a
/// literal `"bash"` would never match any filter branch and every rewritten
/// command would silently pass through with 0% compression — defeating the
/// entire point of this spike. Quoted because the derived name may contain
/// a space (e.g. `"cargo test"`).
/// Test: `rewrite_appends_compress_pipe_for_plain_command`,
/// `rewrite_appends_compress_pipe_with_subcommand_tool_name`,
/// `rewrite_skips_orchestrator_commands`, `rewrite_skips_piped_commands`,
/// `rewrite_skips_chained_commands`, `rewrite_skips_empty_command`,
/// `rewrite_skips_command_substitution_in_tool_name`.
pub
/// Derive the `compress_tool_output` dispatch key from a Bash command.
///
/// Why: The dispatch table in `trusty-agents-common::compress::tool_output`
/// matches per-tool filters by substring against a `tool_name` string shaped
/// like `"cargo test"` / `"git diff"` (see that module's `cargo`/`git`
/// branches) — it has no branch for the literal string `"bash"`. Passing
/// the command's actual program + subcommand lets the existing `cargo
/// test`/`cargo check`/`git diff`/`git log` filters fire for the domains
/// they already cover; commands outside that coverage (e.g. `grep`, `ls`)
/// still pass through unchanged, which matches this repo's own documented
/// filter-coverage gap (`docs/specs/tool-output-interception-seam.md`
/// §Spike) rather than a new regression.
/// What: Strips the same env-assignment/`sudo`/`env`/path noise
/// [`first_command_token`] strips (looping so `env FOO=bar cargo test` and
/// similar multi-prefix combinations resolve correctly — a trusty-review
/// finding, PR #1968: a one-shot `if first == "sudo"` check left the literal
/// `env` command unhandled, e.g. `env FOO=bar cargo test` previously
/// resolved to the wrong tool name `"env FOO=bar"` instead of `"cargo
/// test"`), then returns `"<program> <subcommand>"` when a second
/// whitespace-delimited token follows, else just `<program>`.
/// Test: `effective_tool_name_joins_program_and_subcommand`,
/// `effective_tool_name_single_token_command`,
/// `effective_tool_name_strips_env_and_sudo_noise`,
/// `effective_tool_name_strips_env_command_prefix`.
pub
/// Whether `tool_name` is safe to embed verbatim inside the double-quoted
/// `--tool "<tool_name>"` argument of the rewritten command.
///
/// Why: [`effective_tool_name`] derives its value from whitespace-delimited
/// tokens of the *user-supplied* Bash command with no further sanitisation.
/// Wrapping the value in double quotes only defeats word-splitting — POSIX
/// shells still perform command substitution (`$(...)`, backticks) and
/// variable expansion (`$VAR`) *inside* double-quoted strings, and an
/// embedded `"` would terminate the quoted argument early and let whatever
/// follows be interpreted as new shell syntax. A command whose first two
/// tokens happen to contain any of those characters — e.g.
/// `echo $(touch /tmp/pwned)` — would therefore have that substitution
/// re-evaluated a second time, in a new shell position, as part of building
/// the `--tool` argument (trusty-review-flagged shell-injection risk, PR
/// #1968; confirmed by an independent `review_diff` pass).
/// What: `true` only when every character is ASCII alphanumeric or one of
/// the small set that legitimately appears in tool names/subcommands/paths
/// (space, `-`, `_`, `.`, `/`, `:`) — exactly what `"cargo test"`,
/// `"git diff"`, or `"./scripts/foo.sh"` need. Anything else (`$`,
/// backticks, `"`, `\`, parens, newlines, …) is rejected, and
/// [`rewrite_bash_command_for_compression`] falls back to its safe
/// no-rewrite default rather than risk embedding it.
/// Test: `is_safe_tool_name_allows_typical_tool_names`,
/// `is_safe_tool_name_rejects_shell_metacharacters`.
/// Whether `command`'s first token matches a known orchestrator exclusion.
///
/// Why: Split out from [`rewrite_bash_command_for_compression`] for direct
/// unit testing of the matching rule independent of the rewrite string
/// format.
/// What: Delegates token extraction to [`first_command_token`], then does a
/// case-sensitive exact match against [`ORCHESTRATOR_EXCLUSIONS`]. When
/// [`first_command_token`] can't confidently resolve the real program name
/// (returns `None` — e.g. `sudo -u root make`, where the token after `sudo`
/// is itself a flag, not a program), this conservatively returns `true`
/// (treat as orchestrator-like, i.e. don't rewrite) rather than `false`: a
/// trusty-review-flagged fail-open gap (PR #1968) where "can't tell" used
/// to mean "assume it's safe to rewrite" — backwards for a safety check
/// whose entire purpose is avoiding exactly this kind of unrecognised
/// build-orchestrator invocation.
/// Test: `is_orchestrator_command_matches_known_exclusions`,
/// `is_orchestrator_command_ignores_unrelated_commands`,
/// `is_orchestrator_command_conservatively_true_for_sudo_with_flags`.
/// Extract the effective first command token, stripping noise prefixes.
///
/// Why: A raw `command.split_whitespace().next()` would miss
/// `FOO=bar make build` (env-var prefix), `sudo make install`, and
/// `/usr/bin/make` (absolute path) — all of which should still match
/// `make` for exclusion purposes. Being permissive here trades a little
/// precision for staying on the safe (under-rewrite) side.
/// What: Loops skipping any number of leading `KEY=value`-shaped tokens
/// interleaved with `sudo`/`env` prefixes (so `env FOO=bar make build`
/// resolves to `make` just like `sudo make build` does — see
/// [`effective_tool_name`]'s doc comment for the same `env`-handling gap
/// this mirrors), then returns the basename (post-`/`) of whatever token
/// remains.
/// Returns `None` — "can't confidently resolve" — when the token
/// immediately after `sudo`/`env` is itself flag-shaped (starts with `-`,
/// e.g. `sudo -u root make`, `env -i cmd`): the real program name in that
/// case needs argument-aware parsing this function doesn't do, and
/// [`is_orchestrator_command`] treats `None` as "conservatively assume
/// orchestrator" rather than risk missing a real exclusion (trusty-review
/// finding, PR #1968).
/// Test: `first_command_token_strips_env_assignment`,
/// `first_command_token_strips_sudo`, `first_command_token_strips_path`,
/// `first_command_token_plain_command`,
/// `first_command_token_strips_env_command_prefix`,
/// `first_command_token_none_for_sudo_followed_by_flag`.
pub
/// Whether `token` looks like a `KEY=value` shell environment assignment.
///
/// Why: Shared by [`first_command_token`]; split out so the "what counts as
/// an env assignment" rule has one definition.
/// What: `KEY` must be non-empty, start with a letter or underscore, and
/// contain only alphanumerics/underscores up to the first `=`.
/// Whether `command` already contains pipe/chain/redirection/background
/// composition.
///
/// Why: Appending a second pipe to a command that already pipes/chains
/// (`|`, `&&`, `;`) is the same class of risk the orchestrator exclusion
/// guards against — the design doc explicitly calls out "don't rewrite
/// something that already contains a `|` or `&&`/`;` chain that could break
/// under an additional pipe". Output/input redirection (`>`, `>>`, `<`) is
/// the same class of risk from the opposite direction: a trusty-review
/// finding (PR #1968) noted that `cargo test > out.log`, rewritten to
/// `cargo test > out.log | tm compress --tool "cargo test"`, pipes
/// `tm compress` an *empty* stdin (stdout was already redirected to the
/// file) — the rewrite would silently do nothing useful while altering the
/// command's pipeline/exit-code shape for no benefit. Backgrounding (`&`) is
/// a third variant of the same risk (a further trusty-review finding, PR
/// #1968): `cargo test &` rewritten to `cargo test & | tm compress ...`
/// is invalid shell syntax in most shells, not merely a no-op.
/// What: Simple substring checks — deliberately conservative (a command
/// with `|`/`&`/`;`/`>`/`<` inside a quoted string literal will also be
/// skipped; under-rewriting is the safe failure mode here). A single
/// `contains('&')` check subsumes both the `&&` chain operator and the
/// standalone backgrounding `&` — there's no case where `&` is safe to
/// leave unguarded in this position.
/// Test: `has_unsafe_pipe_composition_detects_pipe`,
/// `has_unsafe_pipe_composition_detects_and_chain`,
/// `has_unsafe_pipe_composition_detects_semicolon`,
/// `has_unsafe_pipe_composition_detects_output_redirection`,
/// `has_unsafe_pipe_composition_detects_input_redirection`,
/// `has_unsafe_pipe_composition_detects_background_operator`,
/// `has_unsafe_pipe_composition_false_for_plain_command`.
/// Build the `hookSpecificOutput.updatedInput` JSON body for a `PreToolUse`
/// Bash command rewrite.
///
/// Why: This is the JSON shape `tm hook` prints to stdout so Claude Code
/// substitutes `rewritten_command` for the original before executing it.
/// Originally implemented per the shape sketched in
/// `docs/specs/tool-output-interception-seam.md` §Option 0 (itself derived
/// from claude-mpm's `ztk_hook.py::_build_ztk_response_impl` convention) with
/// no in-repo reference to confirm it. **Confirmed against the live Claude
/// Code hooks reference** (<https://code.claude.com/docs/en/hooks>,
/// confirmed 2026-07-03 while addressing a trusty-review finding on PR
/// #1968): "PreToolUse: `updatedInput` directly under `hookSpecificOutput`
/// replaces a tool's arguments before it runs," with the documented example
/// matching this shape exactly. That same reference also confirms (a) the
/// hook's response is only parsed on `exit 0`, and stdout "must contain only
/// the JSON object" for that parse to succeed — see `misc::hook`'s doc
/// comment for why the caller now returns immediately after printing this
/// — and (b) the stdin payload field names this module's caller
/// (`misc::hook`) reads (`hook_event_name`, `tool_name`,
/// `tool_input.command`) match the live protocol. This function itself has
/// no failure path: parsing failures upstream (in `misc::read_stdin_hook_payload`)
/// already degrade to "no stdin payload" before this is ever called, so an
/// invalid/unrecognised hook payload shape fails safe as a silent no-op, not
/// a malformed response.
/// What: `{"hookSpecificOutput": {"hookEventName": "PreToolUse",
/// "updatedInput": {"command": rewritten_command}}}`.
/// Test: `build_pretooluse_rewrite_response_has_expected_shape`.
pub