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
//! Start every tm-managed Claude Code session on the classic renderer, with
//! its mouse capture off (issues #6495, #7160).
//!
//! Why: Claude Code's fullscreen renderer takes the alternate screen and
//! captures the mouse wheel, so a managed pane scrolls only the one live window
//! — the terminal's native scrollback and tmux's copy-mode history both stop
//! responding, which operators report as "everything is one window that will not
//! scroll". Claude Code names the escape hatch in its own failure text:
//! `CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 forces that any time`. #7160 found
//! the fix incomplete: even under the classic renderer, Claude Code still sets
//! tmux's per-pane `mouse_any_flag` via its own escape sequence, so the wheel
//! still reaches the app instead of tmux copy-mode — `CLAUDE_CODE_DISABLE_MOUSE`
//! is the documented opt-out for that half. Setting either by hand in
//! `~/.claude/settings.json` fixes a session; tm now provisions both so nobody
//! has to find them.
//!
//! What: [`MANAGED_DEFAULTS`] is the one table — variable name, default value,
//! and pinned shell operand — that drives every carrier below, so a third
//! variable is a one-line addition to the table rather than a change repeated
//! at each launch site:
//! * [`managed_shell_assignments`] composes the `env NAME="${NAME-default}"`
//! operands, space-joined in table order, for the builders that emit an
//! `env …` prefix string.
//! * [`apply_default_to_command`] (and its hermetic core,
//! [`apply_default_when_unset`]) mutates a [`std::process::Command`] for the
//! builders that exec `claude` directly, applying each table entry
//! independently.
//!
//! Both carriers YIELD to a value the launch already carries; neither forces
//! one, and each variable is decided independently — an operator who exported
//! only one of the two still gets the tm default for the other.
//!
//! Operator precedence, and where it comes from:
//! * launch environment — the shell operand expands `${NAME-default}`, so the
//! pane shell substitutes the default only when the variable is UNSET. A
//! value the pane already exports reaches `claude` unchanged, including an
//! empty one (`-`, not `:-`). [`apply_default_when_unset`] makes the same
//! decision in Rust for the exec paths, reading the environment the child
//! would otherwise inherit.
//! * settings `env` block — Claude Code applies an allowlisted settings `env`
//! entry ON TOP of the process environment, and both variables are on that
//! allowlist, so an operator entry in any settings tier already outranks
//! whatever tm exports. tm writes neither variable into any settings file,
//! so there is nothing here for an operator entry to fight.
//!
//! Switching renderers from inside a session still works: Claude Code's own
//! switch re-execs with these variables dropped.
//!
//! Test: this module's `tests`, plus one per launch path for each variable —
//! the daemon's spawn/resume/attach paths by
//! `spec_command_yields_the_alt_screen_default_to_the_pane` (#8233 turned the
//! `${NAME-1}` shell operand into `apply_default_to_command`), and
//! `claude_command_defaults_the_alternate_screen_off` /
//! `claude_command_defaults_the_mouse_capture_off`,
//! `inplace_session_command_defaults_the_alternate_screen_off` /
//! `inplace_session_command_defaults_the_mouse_capture_off`,
//! `client_session_command_defaults_the_alternate_screen_off` /
//! `client_session_command_defaults_the_mouse_capture_off`,
//! `test_build_launch_command_defaults_the_alternate_screen_off` /
//! `test_build_launch_command_defaults_the_mouse_capture_off`,
//! `inplace_exec_command_defaults_the_alternate_screen_off` /
//! `inplace_exec_command_defaults_the_mouse_capture_off`.
/// The Claude Code variable that selects the classic (non-fullscreen) renderer.
pub const ALT_SCREEN_ENV_VAR: &str = "CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN";
/// The value tm provisions when the launch carries none.
pub const ALT_SCREEN_DEFAULT: &str = "1";
/// The `env` operand every shell-string launch line carries (#6495).
///
/// Why: written as a literal rather than composed from
/// [`ALT_SCREEN_ENV_VAR`]/[`ALT_SCREEN_DEFAULT`] so a test can pin the exact
/// shell text. A composed constant compared against a composed expectation
/// would stay green if the `${NAME-default}` form itself were broken — and the
/// form IS the operator-precedence mechanism, not decoration around it.
/// What: `NAME="${NAME-1}"`, which the pane shell expands to the operator's
/// exported value when the variable is set and to `1` when it is not. The
/// `-` form (rather than `:-`) preserves an explicitly-empty operator value.
/// Double quotes are required: the single quotes the sibling assignments use
/// would suppress the expansion and hand `claude` the literal text.
///
/// POSIX `env` grammar puts this AFTER every `-u` flag on the line — see
/// [`crate::core::claude_env_scrub::env_unset_flags`].
/// Test: `shell_assignment_pins_the_defaulting_form`,
/// `shell_assignment_names_the_variable_and_the_default`.
pub const ALT_SCREEN_SHELL_ASSIGNMENT: &str =
"CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=\"${CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN-1}\"";
/// The Claude Code variable that opts the pane out of the app's own mouse
/// capture (#7160).
///
/// Why: tmux's per-pane `mouse_any_flag` is set by the application running in
/// the pane, via an escape sequence — independent of tmux's own `mouse`
/// server option and independent of [`ALT_SCREEN_ENV_VAR`]. Claude Code sets
/// it even under the classic renderer, so #6495's fix alone leaves the wheel
/// captured. Claude Code documents this variable as the opt-out.
pub const MOUSE_ENV_VAR: &str = "CLAUDE_CODE_DISABLE_MOUSE";
/// The value tm provisions when the launch carries none.
pub const MOUSE_DEFAULT: &str = "1";
/// The `env` operand every shell-string launch line carries (#7160) — the
/// mouse-capture counterpart to [`ALT_SCREEN_SHELL_ASSIGNMENT`], same form and
/// same reason for being a pinned literal rather than a composed one.
/// Test: `mouse_shell_assignment_pins_the_defaulting_form`,
/// `mouse_shell_assignment_names_the_variable_and_the_default`.
pub const MOUSE_SHELL_ASSIGNMENT: &str =
"CLAUDE_CODE_DISABLE_MOUSE=\"${CLAUDE_CODE_DISABLE_MOUSE-1}\"";
/// One variable tm provisions a default for on every managed launch.
///
/// Why: pairs each variable's name, default, and pinned shell operand so
/// [`MANAGED_DEFAULTS`] can drive every carrier from one table instead of the
/// table's shape being re-derived at each carrier (#7160).
/// What: three fields, all `&'static str` so the table itself can be a `const`.
/// Every variable tm provisions a default for on a managed launch, in the
/// order every carrier emits them (#6495, #7160).
///
/// Why: the single source of truth [`managed_shell_assignments`] and
/// [`apply_default_when_unset`] both drive off, so a third variable is a
/// one-line addition here rather than a change repeated at every launch site
/// this module has callers in.
/// Test: `managed_shell_assignments_joins_both_operands_in_order`.
const MANAGED_DEFAULTS: & = &;
/// The `env` operand text carrying every managed default, space-joined in
/// [`MANAGED_DEFAULTS`] order — what a shell-command launch line splices in
/// ahead of `claude` (#6495 alternate-screen, #7160 mouse capture).
///
/// Why: every shell-string builder previously spliced in
/// [`ALT_SCREEN_SHELL_ASSIGNMENT`] alone; adding the mouse default without a
/// second call at every one of those sites would silently miss one the next
/// time a variable is added. One function, driven by the table, is the single
/// place a launch line's defaulted-env text is assembled.
/// What: each entry's `shell_assignment`, joined with a single space — the
/// same separator every builder already places between the scrub flags and
/// the assignment, and between one assignment and the next (see
/// [`crate::core::model_inject::build_claude_command_with`]'s per-assignment
/// `cmd.push(' ')`).
/// Test: `managed_shell_assignments_joins_both_operands_in_order`.
/// Provision every managed default (#6495, #7160) on a `claude`
/// [`std::process::Command`].
///
/// Why: the exec launch paths (the bare-`tm` in-place relaunch, `tm run`) build
/// a `Command` with no shell in between, so [`managed_shell_assignments`] and
/// its `${NAME-default}` expansion cannot apply to them — but they inherit the
/// same pane environment and need the same defaults and the same operator
/// precedence, decided independently per variable.
/// What: delegates to [`apply_default_when_unset`] with a real-environment
/// lookup. The parent's environment is what the child inherits, so "already set
/// here" is exactly "the launch already carries a value".
/// Test: covered via [`apply_default_when_unset`], which takes the lookup as a
/// parameter — mirroring
/// [`crate::core::claude_env_scrub::markers_present_in_env`], and for the same
/// reason: `std::env::set_var` is `unsafe` in edition 2024 and mutates state
/// every test in the binary shares.
/// Pure core of [`apply_default_to_command`]: for every table entry, set its
/// default only when `is_set` reports that variable absent.
///
/// Why: [`std::process::Command::env`] overrides the inherited value
/// unconditionally, so an unguarded call would defeat an operator who exported
/// the variable in the pane — the exact override #6495/#7160 must not perform.
/// Each variable is decided independently: an operator who exported only one
/// of the two must still get the tm default for the other, never both-or-
/// neither.
/// What: for each entry in [`MANAGED_DEFAULTS`], no-op when
/// `is_set(entry.env_var)` is true; otherwise
/// `cmd.env(entry.env_var, entry.default)`.
/// Test: `command_defaults_apply_when_every_variable_is_unset`,
/// `command_defaults_yield_to_an_operator_value`,
/// `command_defaults_yield_per_variable_independently`.