claude-telegram-mirror 0.2.22 → 0.2.24

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/README.md CHANGED
@@ -22,7 +22,7 @@ This installs a native Rust binary (`ctm`) via platform-specific optional packag
22
22
  - **CLI to Telegram**: Mirror Claude's responses, tool usage, and notifications
23
23
  - **Telegram to CLI**: Send prompts from Telegram directly to Claude Code
24
24
  - **Tool Summarizer**: Human-readable summaries for 30+ command patterns ("Running tests" instead of "Running: Bash")
25
- - **AskUserQuestion (no keystrokes)**: Inline buttons for Claude's interactive questions; option answers are returned to Claude structurally via the hook's `updatedInput` instead of fragile keystroke injection
25
+ - **AskUserQuestion (both surfaces)**: Claude's interactive multiple-choice question renders natively in the CLI *and* as inline buttons on Telegram — answer from either side. Telegram answers drive the live CLI widget via `tmux send-keys`, with `capture-pane` readiness pacing on multi-select so the confirming Enter only fires once Claude's review screen is on-screen (ADR-015)
26
26
  - **Photo & Document Upload**: Send images/files from Telegram, path injected into Claude
27
27
  - **Stop/Interrupt**: Type `stop` to send Escape, `kill` to send Ctrl-C
28
28
  - **Session Threading**: Each Claude session gets its own Forum Topic
@@ -153,10 +153,10 @@ Approval buttons only appear in normal mode, not with `--dangerously-skip-permis
153
153
 
154
154
  **Flow:**
155
155
  1. Claude Code hooks invoke `ctm hook`, which reads the event from stdin
156
- 2. PreToolUse: sends an approval request (or, for AskUserQuestion, a question request) via socket and blocks for the Telegram response
156
+ 2. PreToolUse: for tool approvals, sends an approval request via socket and blocks for the Telegram response. **AskUserQuestion is *not* intercepted here** the hook returns fast so Claude renders its native widget in the CLI; the daemon mirrors the question to Telegram from the standard `tool_start` event (ADR-015)
157
157
  3. Other hooks: sends JSON to daemon via socket and exits immediately
158
158
  4. Daemon forwards messages to Telegram Forum Topic with summarized tool actions
159
- 5. Telegram text replies are injected into the CLI via `tmux send-keys`. **AskUserQuestion option/multi-select answers are returned structurally via the hook's `updatedInput` — no keystrokes** (only free-text answers fall back to injection)
159
+ 5. Telegram replies are injected into the live CLI via `tmux send-keys` — including AskUserQuestion option/multi-select answers, which drive the native widget directly. **Multi-select uses `tmux capture-pane` readiness pacing** so each keystroke and the confirming Enter only fire once the expected screen has rendered — no blind sleeps (ADR-015). The question shows in **both** the CLI (native) and Telegram, answerable from either
160
160
  6. Stop/kill commands send Escape or Ctrl-C to interrupt Claude
161
161
 
162
162
  ## Multi-System Architecture
@@ -301,7 +301,7 @@ ctm install-hooks --project
301
301
  - **Topic routing**: Each daemon only processes topics it created (multi-bot safe)
302
302
  - **Rate limiting**: Governor-based with exponential backoff retry queue
303
303
  - **Token scrubbing**: All log output filtered through regex to strip bot tokens
304
- - **Test suite**: 512 Rust tests (unit + 10 integration test files)
304
+ - **Test suite**: 470+ Rust tests (unit + 11 integration test files)
305
305
 
306
306
  ## Troubleshooting
307
307
 
@@ -367,7 +367,7 @@ cargo test
367
367
  ./target/release/ctm start
368
368
  ```
369
369
 
370
- ### Project Structure (30 source files)
370
+ ### Project Structure (33 source files)
371
371
 
372
372
  ```
373
373
  rust-crates/ctm/src/
@@ -382,16 +382,19 @@ rust-crates/ctm/src/
382
382
  injector.rs # tmux input injection
383
383
  formatting.rs # Message formatting, chunking, ANSI stripping
384
384
  summarize.rs # Tool action summarizer (30+ patterns)
385
+ liveness.rs # tmux pane liveness checks for topic reconciliation
386
+ prune.rs # prune-topics subcommand (bulk stale-topic cleanup)
385
387
  colors.rs # ANSI color helpers for terminal output
386
388
  doctor.rs # Diagnostic checks with --fix
387
389
  installer.rs # Hook installer
388
390
  setup.rs # Interactive setup wizard
389
- bot/ # Telegram API client (client.rs, queue.rs, types.rs)
391
+ bot/ # Telegram API client (mod.rs, client.rs, queue.rs, types.rs)
390
392
  daemon/ # Bridge daemon (mod.rs, event_loop.rs, socket_handlers.rs,
391
- # telegram_handlers.rs, callback_handlers.rs, cleanup.rs, files.rs)
393
+ # telegram_handlers.rs, callback_handlers.rs, cleanup.rs,
394
+ # reconcile.rs, files.rs)
392
395
  service/ # OS service management (mod.rs, systemd.rs, launchd.rs, env.rs)
393
396
 
394
- rust-crates/ctm/tests/ # 10 integration test files
397
+ rust-crates/ctm/tests/ # 11 integration test files
395
398
  ```
396
399
 
397
400
  </details>
package/SECURITY.md CHANGED
@@ -62,11 +62,11 @@ metacharacters are rejected.
62
62
 
63
63
  ### 2. Bot Token Scrubbing
64
64
 
65
- **File:** `rust-crates/ctm/src/bot/client.rs`
65
+ **Files:** `rust-crates/ctm/src/main.rs` (subscriber layer), `rust-crates/ctm/src/bot/mod.rs` (regex)
66
66
 
67
- A `tracing` subscriber layer applies `scrub_bot_token()` to log output.
68
- The regex `bot\d+:[A-Za-z0-9_-]+/` matches the Telegram bot token pattern
69
- in API URLs and replaces it with `bot<REDACTED>`.
67
+ A `tracing` subscriber layer (`ScrubWriter` in `main.rs`) applies `scrub_bot_token()`
68
+ to all log output. The regex `bot\d+:[A-Za-z0-9_-]+/` matches the Telegram bot token
69
+ pattern in API URLs and replaces it with `bot[REDACTED]/`.
70
70
 
71
71
  All log output goes to stderr via the `tracing` subscriber. There is no
72
72
  file transport, so tokens cannot leak into log files on disk.
@@ -79,8 +79,9 @@ tokens from error messages before logging.
79
79
  **File:** `rust-crates/ctm/src/daemon/telegram_handlers.rs`
80
80
 
81
81
  A chat ID check verifies `chat.id` against the configured `chat_id` on
82
- every incoming update. Updates from unauthorized chats receive a static
83
- "Unauthorized" reply and are not processed further.
82
+ every incoming update. Updates from unauthorized chats are silently dropped
83
+ (logged as a warning, no reply) by design, since replying would confirm the
84
+ bot's existence and function to an attacker (ADR-006 L4.6).
84
85
 
85
86
  Approval callback handlers (`approve:`, `reject:`, `abort:`), answer
86
87
  handlers (`answer:`, `toggle:`, `submit:`), all verify the chat ID
@@ -94,7 +95,7 @@ different chat.
94
95
 
95
96
  Session IDs from hook events are validated before any database operation:
96
97
  - Maximum length: 128 characters
97
- - Character set: `[a-zA-Z0-9_-]` only
98
+ - Character set: `[a-zA-Z0-9_.-]` only
98
99
  - Empty/null values are rejected
99
100
 
100
101
  Messages with invalid session IDs are dropped with a warning log.
@@ -118,10 +119,10 @@ socket.
118
119
 
119
120
  **File:** `rust-crates/ctm/src/config.rs`
120
121
 
121
- `validateSocketPath()` rejects socket paths that:
122
+ `validate_socket_path()` rejects socket paths that:
122
123
  - Contain `..` (directory traversal)
123
124
  - Are not absolute (do not start with `/`)
124
- - Exceed 256 characters
125
+ - Exceed 104 characters (the AF_UNIX `sun_path` limit)
125
126
 
126
127
  Invalid paths fall back to the default socket path in the config directory.
127
128
 
@@ -151,7 +152,7 @@ handler logs a warning and exits cleanly without processing the payload.
151
152
 
152
153
  ### 10. Download File Handling
153
154
 
154
- **File:** `rust-crates/ctm/src/daemon/telegram_handlers.rs`
155
+ **File:** `rust-crates/ctm/src/daemon/files.rs`
155
156
 
156
157
  Downloaded files from Telegram are handled with several protections:
157
158
  - The downloads directory is created with mode 0o700
@@ -179,11 +180,11 @@ Downloaded files from Telegram are handled with several protections:
179
180
 
180
181
  | Boundary | Validation | Enforcement |
181
182
  |---|---|---|
182
- | Socket messages: session ID | 128 char max, `[a-zA-Z0-9_-]` | `socket_handlers.rs` — `is_valid_session_id()` |
183
+ | Socket messages: session ID | 128 char max, `[a-zA-Z0-9_.-]` | `socket_handlers.rs` — `is_valid_session_id()` |
183
184
  | Socket lines | 1 MiB max per NDJSON line | `socket.rs` — `MAX_LINE_BYTES` |
184
185
  | Socket connections | 64 concurrent max | `socket.rs` — `MAX_CONNECTIONS` |
185
186
  | Hook stdin | 1 MiB max | `hook.rs` — `MAX_STDIN_BYTES` |
186
- | Socket paths | No `..`, absolute only, 256 char max | `config.rs` — `validate_socket_path()` |
187
+ | Socket paths | No `..`, absolute only, 104 char max | `config.rs` — `validate_socket_path()` |
187
188
  | Slash commands | Character whitelist: `[a-zA-Z0-9_- /]` | `injector.rs` — `is_valid_slash_command()` |
188
189
  | Download filenames | Sanitized, UUID-prefixed, no `..`, 200 char max | `telegram_handlers.rs` — `sanitize_filename()` |
189
190
  | Download file size | 20 MB max | Telegram Bot API server-side limit |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-telegram-mirror",
3
- "version": "0.2.22",
3
+ "version": "0.2.24",
4
4
  "description": "Bidirectional Telegram integration for Claude Code CLI - monitor and control your Claude Code sessions from Telegram",
5
5
  "bin": {
6
6
  "claude-telegram-mirror": "./scripts/ctm-wrapper.cjs",
@@ -49,10 +49,10 @@
49
49
  "access": "public"
50
50
  },
51
51
  "optionalDependencies": {
52
- "@agidreams/ctm-linux-x64": "0.2.22",
53
- "@agidreams/ctm-linux-arm64": "0.2.22",
54
- "@agidreams/ctm-darwin-arm64": "0.2.22",
55
- "@agidreams/ctm-darwin-x64": "0.2.22"
52
+ "@agidreams/ctm-linux-x64": "0.2.24",
53
+ "@agidreams/ctm-linux-arm64": "0.2.24",
54
+ "@agidreams/ctm-darwin-arm64": "0.2.24",
55
+ "@agidreams/ctm-darwin-x64": "0.2.24"
56
56
  },
57
57
  "engines": {
58
58
  "node": ">=18.0.0"
package/postinstall.cjs CHANGED
@@ -7,6 +7,71 @@
7
7
  const os = require('os');
8
8
  const path = require('path');
9
9
  const fs = require('fs');
10
+ const { execFileSync } = require('child_process');
11
+
12
+ /**
13
+ * macOS hardening: a binary delivered via npm carries only the ad-hoc signature
14
+ * the build toolchain applied — it is NOT Developer-ID signed or notarized. On
15
+ * modern macOS the kernel can SIGKILL such a binary for a code-signing /
16
+ * launch-constraint violation (EXC_CRASH / "Code Signature Invalid"),
17
+ * especially on the first launchd-spawned launch after install. This pass is
18
+ * defense-in-depth: strip any quarantine flag, ensure the binary has a coherent
19
+ * (at least ad-hoc) signature, and smoke-test that it can actually exec. It
20
+ * never throws — install must still succeed — but it warns loudly so the user
21
+ * is not left with a binary the OS silently refuses to run.
22
+ */
23
+ function hardenMacBinary(binary) {
24
+ if (process.platform !== 'darwin') return;
25
+
26
+ const run = (cmd, args) => {
27
+ try {
28
+ return { ok: true, out: execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }) };
29
+ } catch (e) {
30
+ return { ok: false, out: (e.stdout || '') + (e.stderr || ''), err: e };
31
+ }
32
+ };
33
+
34
+ // 1. Remove the quarantine xattr if a download path applied one. Best-effort.
35
+ run('xattr', ['-d', 'com.apple.quarantine', binary]);
36
+
37
+ // 2. If the signature is missing/invalid, re-apply an ad-hoc signature so the
38
+ // Mach-O is at least internally consistent and execable on Apple Silicon.
39
+ const verify = run('codesign', ['--verify', '--strict', binary]);
40
+ if (!verify.ok) {
41
+ const resign = run('codesign', ['--force', '--sign', '-', binary]);
42
+ if (!resign.ok) {
43
+ console.log('WARNING: Could not re-sign the native binary; macOS may refuse to run it.');
44
+ console.log(' Try manually: codesign --force --sign - "' + binary + '"');
45
+ }
46
+ }
47
+
48
+ // 3. Smoke-test exec. If the OS kills it here, the user would otherwise only
49
+ // discover it when the daemon silently fails to start.
50
+ const smoke = run(binary, ['--version']);
51
+ if (!smoke.ok) {
52
+ console.log('');
53
+ console.log('WARNING: The native ctm binary failed to execute on this machine.');
54
+ console.log(' This is typically a macOS code-signing / Gatekeeper rejection of a');
55
+ console.log(' non-notarized binary. To inspect:');
56
+ console.log(' codesign -dvvv "' + binary + '"');
57
+ console.log(' ls ~/Library/Logs/DiagnosticReports/ctm-*.ips');
58
+ console.log(' Or build from source: cd rust-crates && cargo build --release');
59
+ console.log('');
60
+ }
61
+ }
62
+
63
+ // Protect the native binary on every real install — independent of whether we
64
+ // also print the setup guidance below. Skipped under CI (the binary is built
65
+ // and verified there, not consumed).
66
+ if (!process.env.CI) {
67
+ try {
68
+ const { resolveBinary } = require('./scripts/resolve-binary.cjs');
69
+ const r = resolveBinary();
70
+ if (r) hardenMacBinary(r.binary);
71
+ } catch {
72
+ // resolve-binary unavailable — skip silently
73
+ }
74
+ }
10
75
 
11
76
  // Don't show guidance during CI or if TELEGRAM_BOT_TOKEN is already set
12
77
  if (process.env.CI || process.env.TELEGRAM_BOT_TOKEN) {