repos / zmx

session persistence for terminal processes
git clone https://github.com/neurosnap/zmx.git

commit
a9dc983
parent
46b02ee
author
Patrik Sundberg
date
2026-03-17 13:29:14 -0400 EDT
fix(attach): reject combined intentional modifiers for Ctrl+\ detach

The parser accepted any modifier combination that included ctrl
(ctrl+shift, ctrl+alt, ctrl+super, etc.) because it only checked
the ctrl bit. This was over-permissive: ctrl+shift+\ is a different
key combination and should not trigger detach.

Tighten the check to require ctrl as the ONLY intentional modifier.
Lock modifiers (caps_lock, num_lock) remain tolerated since they are
ambient state, not deliberate key combinations.
1 files changed,  +17, -6
M src/util.zig
+17, -6
 1@@ -246,8 +246,11 @@ fn parseKittyCtrlBackslash(buf: []const u8) bool {
 2     if (mod_encoded < 1) return false;
 3     const mod_raw = mod_encoded - 1;
 4 
 5-    // 5. Ctrl bit (0b100 = 4) must be set.
 6-    if (mod_raw & 0b100 == 0) return false;
 7+    // 5. Ctrl must be the only intentional modifier. Lock modifiers
 8+    //    (caps_lock=0b1000000, num_lock=0b10000000) are tolerated because
 9+    //    they are ambient state, not deliberate key combinations.
10+    const intentional_mods = mod_raw & 0b00111111;
11+    if (intentional_mods != 0b100) return false;
12 
13     // 6. Parse optional event type after ':'.
14     if (pos < buf.len and buf[pos] == ':') {
15@@ -634,14 +637,22 @@ test "isKittyCtrlBackslash" {
16     // ctrl + caps_lock + num_lock = 1 + (4 + 64 + 128) = 197
17     try expect(isKittyCtrlBackslash("\x1b[92;197u"));
18 
19-    // Combined modifiers: ctrl + shift = 1 + (4 + 1) = 6
20-    try expect(isKittyCtrlBackslash("\x1b[92;6u"));
21+    // Combined intentional modifiers — must NOT match (ctrl+\ is the
22+    // detach key, not ctrl+shift+\ or ctrl+alt+\)
23+    // ctrl + shift = 1 + (4 + 1) = 6
24+    try expect(!isKittyCtrlBackslash("\x1b[92;6u"));
25 
26     // ctrl + alt = 1 + (4 + 2) = 7
27-    try expect(isKittyCtrlBackslash("\x1b[92;7u"));
28+    try expect(!isKittyCtrlBackslash("\x1b[92;7u"));
29 
30     // ctrl + super = 1 + (4 + 8) = 13
31-    try expect(isKittyCtrlBackslash("\x1b[92;13u"));
32+    try expect(!isKittyCtrlBackslash("\x1b[92;13u"));
33+
34+    // ctrl + shift + caps_lock = 1 + (1 + 4 + 64) = 70 — shift is intentional
35+    try expect(!isKittyCtrlBackslash("\x1b[92;70u"));
36+
37+    // ctrl + shift + num_lock = 1 + (1 + 4 + 128) = 134 — shift is intentional
38+    try expect(!isKittyCtrlBackslash("\x1b[92;134u"));
39 
40     // Modifier without ctrl bit — must NOT match
41     // shift only = 1 + 1 = 2