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
//! The `PreToolUse` bulk-read diversion hook groups (#6887).
//!
//! Why: a session that `Read`s a 2000-line file pays for every one of those
//! lines in its own context on its own expensive model. Spotify's `shunt`
//! plugin intercepts that read with a `PreToolUse` hook and steers the agent to
//! a cheap worker instead; the #6882 POC measured -45% cost and -54% output
//! tokens on this repo. This module owns the two hook groups that wire the
//! interception, kept OUT of
//! [`super::project_hooks`] and [`super::settings`] because both sit close to
//! the 500-SLOC production cap and because the identity predicate, the command
//! string, and the group shape are one concern that should move together.
//!
//! What: [`divert_hook_groups`] builds the two `PreToolUse` handler groups
//! (matcher `"Read"`, matcher `"Bash"`) invoking `<abs-path> hook
//! --divert-check`; [`is_divert_hook_command`] is the matching identity
//! predicate [`super::project_hooks::is_project_managed_hook_command`] folds in
//! so the replace-by-identity strip removes a stale entry when `[divert]
//! enabled` flips back to false.
//! Test: `project_hooks_tests.rs`
//! (`project_managed_hook_additions_includes_divert_when_enabled`,
//! `project_managed_hook_additions_omits_divert_when_disabled`,
//! `is_project_managed_hook_command_recognises_divert_check`,
//! `write_project_hooks_strips_stale_divert_when_disabled`).
use resolve_statusline_binary;
/// The `tm hook` sub-flag the diversion check is invoked with.
///
/// Why: the identity predicate and the command builder must agree on one
/// spelling, or the strip stops recognising what the writer wrote and every
/// relaunch appends a duplicate group (the #2948 failure mode). #7262 added a
/// THIRD reader — the build-tree classifier, which must recognise this shape
/// too — so the literal now lives in the lower layer beside the other argv
/// tails and is re-exported here rather than spelled out twice.
/// What: the literal suffix, including its leading space.
/// Test: `is_project_managed_hook_command_recognises_divert_check`.
pub use crateDIVERT_CHECK_SUFFIX;
/// Tool names the diversion hook is registered for.
///
/// Why: bulk READS only (#6887 scope). `Read` is Claude Code's own file
/// reader; `Bash` covers `cat`/`head`/`tail`/`less`/`more`, which reach the
/// same bytes by another route. Write diversion is explicitly a follow-up, so
/// no edit tool appears here.
/// What: the two `matcher` values, in the order they are appended.
/// Test: `project_managed_hook_additions_includes_divert_when_enabled`.
const DIVERT_MATCHERS: = ;
/// Build the `PreToolUse` handler groups that register the diversion check.
///
/// Why: this is the single composition point for the hook's command string, so
/// [`is_divert_hook_command`] has exactly one shape to recognise. The binary is
/// resolved to an ABSOLUTE path via
/// [`resolve_statusline_binary`] for the same reason the PM guard is (#1914): a
/// bare `tm` silently no-ops under Claude Code's minimal `PATH`, which would
/// leave the hook un-fired and the feature invisibly dead.
/// What: returns one group per [`DIVERT_MATCHERS`] entry, each invoking
/// `<abs-path> hook --divert-check` with the same 10-second timeout the PM
/// guard uses. NO credential and no worker model appear in the command string
/// — the resolved config reaches the hook through the session's environment
/// (see [`crate::core::mcp_session_env`]), and credentials are resolved inside
/// the `tm` subprocess at invocation time.
/// Test: `project_managed_hook_additions_includes_divert_when_enabled`.
pub
/// Recognise a diversion-check hook command.
///
/// Why (#5034 lesson, restated for this toggle): without this arm the
/// replace-by-identity strip in
/// [`super::settings::write_project_hooks`] never removes the groups a
/// previous launch wrote, so setting `[divert] enabled = false` would leave
/// them firing forever — and, because the strip also dedupes, every relaunch
/// with the toggle ON would append a second copy instead of replacing the
/// first.
/// What: returns `true` when `cmd` ends with [`DIVERT_CHECK_SUFFIX`]. Note this
/// is deliberately NOT covered by
/// [`crate::core::standalone::hooks::is_mpm_hook_command`], which requires the
/// command to end with exactly ` hook`.
/// Test: `is_project_managed_hook_command_recognises_divert_check`.
pub