Skip to content

UTC timestamps, animated GIFs, multi-file sends, and a 2-4x faster bridge - #47

Closed
zachwilke wants to merge 7 commits into
nixfred:mainfrom
zachwilke:perf-media-and-utc
Closed

zachwilke wants to merge 7 commits into
nixfred:mainfrom
zachwilke:perf-media-and-utc

Conversation

@zachwilke

Copy link
Copy Markdown
Contributor

Seven commits: a correctness fix for timestamps, two media/compose fixes, three
performance changes, and one docs correction. Rebased on main (including the
compose Flickable and per-conversation drafts). CI-equivalent checks are green
locally: 420 bun test, all seven Mac-tool tests including test_cluster,
py_compile, bash -n. Deployed and running on my Omarchy box.

Three things below need your call, not just review — please read those first.

What's in it

1. Time crosses the bridge as UTC (688f2cb)

Stamps arrived as the Mac's naive wall clock and were compared against the
Linux clock — the same string only while both machines sit in one timezone. Two
ways it lied. A Mac an hour ahead put every read mark ahead of every message,
so nothing ever counted as unread. And once a year, in the DST fall-back hour,
the Mac's own clock repeats: two messages an hour apart carry the SAME stamp,
so ordering, the watermark and "newer than the mark" stop meaning anything for
that hour. Every a.ts < b.ts in the collector assumes lexical order is
chronological order; only UTC actually provides it.

imsg's JSON is now ISO-8601 UTC; the plain-text renders keep the Mac's clock
(fmt_ts_local), because a person reading imsg recent wants the time they
remember. Labels convert to the reader's zone at render, so day dividers still
break at your midnight.

Upgrading is covered from both ends, and both halves are load-bearing together:
loadState() migrates marks a pre-UTC release wrote, and stamps are normalised
at the two fetch doors so a Mac still on the old bridge keeps working.
Migrating the marks alone would sort every message below every mark (" " <
"T") and silently empty the badge and the toasts.

The suite runs pinned to TZ=UTC, where all of this is invisible; the new
timezone.test.ts and bridge/mac/test_wire_time.py set their own zones and
are what actually cover it.

2. GIFs move (1feab93)

An animated GIF was flattened twice. fetch.ts treats any image/* under the
auto-fetch cap as a preview candidate and asks the Mac to resample it with
sips, which collapses an animation to one frame — a 1.4 MB GIF reached Linux
as a 198 KB JPEG. And a QML Image paints one frame whatever you hand it.
Animated mimes now skip the resample and render through AnimatedImage; stills
stay on Image, which is what applies autoTransform (the EXIF fall-back).
AnimatedImage honours sourceSize (measured), so the decode ceiling still
applies. imageMetrics also learned the GIF header — it reported 0x0 before.

3. A message can carry several files (20f8bc1)

Dropping five photos attached one and discarded four in silence: the handler
read urls[0] and the draft was a single path. Drafts are a list now, ship one
part per file, caption on the first only, service captured when the batch
starts so switching threads mid-send cannot push a later part onto a different
service. A failing part stops the batch and leaves the rest attached. Capped at
ten. Chips are one per row, for the implicit-width reason the received ones are.

Note the rebase: main now uses drafts for the per-conversation TEXT draft
map, so the attachment list is attachDrafts / attachCount /
addAttachment(s) / removeAttachment / clearAttachments. Two properties
named drafts would have been a duplicate-property error.

4. Performance (0727e91, 5b8b01e, 934ee1f)

Nothing was slow; everything paid startup. A bridge call cost ~133 ms while the
SQL underneath ran in ~1 ms: 16 ms ssh, ~25 ms blip-dispatch Python start,
~20 ms imsg's own, 27 ms to open a 218 MB chat.db.

  • The shim fired a second full ssh round trip before every call as a
    connectivity probe — 41 ms, ~31% — bought only for a friendlier error. ssh
    reports transport failure as 255 and every caller already treats 255 exactly
    like 69, so the probe is gone and the shim translates. This also retires the
    trap that came with it: the probe HAD to be ssh -n or it ate stdin and
    emptied --file-stdin payloads. Verified with a stub ssh that stdin still
    reaches the remote command, that a transport failure exits 69, and that other
    exit codes pass through.
  • imsg serve answers many requests on one process with chat.db already open,
    re-dispatched through the SAME parser the CLI uses so there is no second
    command table to drift. Read-only queries only; attachment/avatar stream
    binary and would park the channel behind a 100 MB photo, watch blocks by
    design. blip-bridged holds two such channels behind a unix socket.
  • A handle with no exact last-ten key was compared against all 442 saved
    numbers — 298 conversations meant 60,112 _same_number calls. Bucketed by
    last-seven-digits, which is sound rather than convenient: the rule only
    matches on a suffix with a seven-digit floor, so the last seven always agree.
    Verified identical to the old full scan over all 750 distinct handles in a
    real chat.db, zero mismatches.

Measured, warm medians: opening a conversation 223 -> 83 ms, a poll
160 -> 39 ms, a deep poll 707 -> 342 ms, a bridge call 132 -> 14 ms.
The push debounce came down 250 -> 60 ms and the post-send reload 600 -> 250 ms.

Three "obvious" fixes the measurements talked me out of, all recorded in
CLAUDE.md so nobody retries them: replacing cmd_chats' 300-query N+1 with a
window function is slower (70 -> 107 ms; the per-chat lookups are indexed),
bundling/compiling the TypeScript changes nothing (44 vs 44 ms; a compiled
binary is worse at 53 ms), and python3 -S saves 2 ms.

5. Docs correction (586a4de)

CLAUDE.md said sending tapbacks needs SIP-off injection. That predates
macOS 26: enumerating Messages' menu bar over System Events finds real items —
Edit > Tapback Message..., Reply to Message..., Edit Last Message....
That is the same shape imsg-read used to overturn "marking read is
impossible". Nothing is claimed to work; the note records what is still
unproven (the items read enabled=false from the background, which by our own
read-push finding is evidence of nothing; the item acts on the SELECTED
message, and selecting an arbitrary bubble from Linux is the open problem).
Recorded because a stale "not possible" stops people looking for years.

Your call

  1. blip-bridged is a Linux-side daemon, and ROADMAP retired blipd. I
    think this is a different animal — a transport multiplexer, not an
    application daemon; it holds no state, the leader BarWidget supervises it
    exactly as it already supervises the push watcher, and it is an accelerator
    that nothing depends on: no socket, no socat, a dead daemon or an
    unparseable frame, and bridgeRun() falls through to the ordinary one-shot
    path. But it is your architecture call, and I would rather you reject it
    than discover it later. The other two perf commits stand alone without it.

  2. bridge/mac/imsg is vendored, and I changed it a lot (UTC, serve, the
    contact index). CONTRIBUTING says fixes to imsg are welcome here, but these
    arguably belong upstream in claude-on-mac, and BRIDGE-VERSION now names a
    pin the file no longer matches. sync-bridge.sh will flag the divergence
    and stage an .upstream copy rather than clobbering, which is the intended
    behaviour — but you may want these landed in claude-on-mac first and
    re-vendored instead.

  3. socat becomes a soft dependency for the fast path only. Without it
    Blip behaves exactly as before, just slower; blip-setup says so rather
    than failing. Happy to drop it for a different client if you would rather
    not add one.

Not verified

  • The multi-file chip stack and the GIF bubble are covered by tests and the GIF
    is proven animating from instrumentation (playing=true frameCount=18, frame
    counter cycling, ten successive screenshots all distinct), but I have not
    photographed the chip stack.
  • Tested against one Mac/Linux pair only (macOS 26.6, 218 MB chat.db, ~3,700
    contacts). The timing numbers are from that machine.
  • Nothing here was tested by sending to another person — sends went to the
    self-thread or not at all, per CONTRIBUTING.

Happy to split this into separate PRs if the bundle is too big to review; the
perf commits are sequential and share bridge/mac/imsg, which is why they
arrived together.

🤖 Generated with Claude Code

https://claude.ai/code/session_015xxS9WCT7ifHhbDjYyGnHS

zachwilke and others added 7 commits September 7, 2026 20:13
Stamps arrived as the Mac's naive wall clock ("2026-09-07 14:33:12") and
were compared against the Linux clock — the same string only while both
machines sat in one timezone. A Mac an hour ahead put every read mark ahead
of every message, so nothing ever counted as unread; an hour behind and the
backlog re-toasted. And once a year, in the DST fall-back hour, the Mac's
own clock repeats: two messages an hour apart carried the SAME stamp, so
ordering, the watermark and "newer than the mark" stopped meaning anything
for that hour. Every `a.ts < b.ts` in the collector was already assuming
lexical order equals chronological order; only UTC actually provides it.

imsg's JSON now emits ISO-8601 UTC (fmt_ts); the plain-text renders keep the
Mac's clock (fmt_ts_local), because a person reading `imsg recent` wants the
time they remember. On the Linux side localNowTs() became nowTs() in the same
format, and every label converts to the READER's zone at render: localDay()
and formatStamp() in thread.ts, stampMs()/localDay()/fmtTime() in BlipView.
Day dividers no longer slice the date out of the string — that is UTC's day,
and the divider belongs at your midnight.

Upgrading is covered from both ends, and both halves are load-bearing
together: loadState() migrates the marks a pre-UTC release wrote, and stamps
are normalised at the two fetch doors so a Mac still on the old bridge keeps
working. Migrating the marks alone would have sorted every message below
every mark (" " < "T") and silently emptied the badge and the toasts.

The suite runs pinned to TZ=UTC, where all of this is invisible; the new
timezone.test.ts and bridge/mac/test_wire_time.py set their own zones and
are what actually cover it. Verified live against the gateway Mac on both
the old and the new bridge: 298 threads, unread unchanged, no toast storm
on migration, and 23:47:54Z rendering as 6:47 PM in CDT.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xxS9WCT7ifHhbDjYyGnHS
A GIF was flattened twice on the way to the screen. fetch.ts treats any
image/* under the auto-fetch cap as a preview candidate and asks the Mac to
resample it with sips — which collapses an animation to one frame, so a
1.4 MB GIF landed as a 198 KB JPEG with the motion already gone. Animated
formats now skip that path entirely and cross as their original bytes, into
the `orig` cache slot a click already shares.

The second flattening was in QML: Image paints a single frame whatever you
give it. Animated mimes now render through AnimatedImage. Stills deliberately
stay on Image — it is the one that applies autoTransform, the EXIF fall-back
for anything cached before fetch.ts started baking orientation in. Sizing is
computed once on a wrapping Item so the two renderers can never disagree
about the delegate's width, and only the active one takes a source, or every
photo would decode twice. AnimatedImage honours sourceSize (measured), so the
decode ceiling still applies to a huge GIF.

imageMetrics also learned the GIF header: it reported 0×0 before, which left
the bubble with nothing to compute a height from.

Verified against a real 498×372, 18-frame, infinitely-looping GIF in chat.db:
the bridge now delivers all 1,357,761 bytes instead of a 198 KB still, and
the panel reports animated=true playing=true frameCount=18 with the frame
counter cycling 0→6→12→0 while ten successive screenshots of the bubble come
back as ten distinct images.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xxS9WCT7ifHhbDjYyGnHS
…opped

Dropping five photos attached one and discarded four in silence — the drop
handler took urls[0] and draftPath was a single string. A drop that loses
most of what you dropped, with no message, is worse than one that refuses.

Drafts are a list now. Drag-and-drop queues every file it was given, /attach
and Ctrl+V append rather than replace, and each chip removes only itself.
Sends go out one part per file in queue order through the existing
send-file.ts path (unchanged: still one file per invocation, still stdin,
still refusing groups whose guid is unknown). The caption rides the first
part alone, or five files would post the same sentence five times.

Two things a batch has to survive. The service is captured when it STARTS,
never re-read from root.active per part, so switching threads mid-send cannot
push a later part onto a different service (war room #2). And a failing part
stops the batch, leaves the remaining files attached and says how many are
left, instead of making the user work out which of five actually went.

Capped at ten files: a stray drop of a whole folder should be refused, not
turned into eighty sends. Chips are one per row for the reason the received
ones are — a RowLayout of N sums implicit widths and drags the conversation
column past the panel edge, taking every right-aligned element with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xxS9WCT7ifHhbDjYyGnHS
Blip felt subtly laggy rather than slow. Measuring said why: nothing is slow,
everything pays startup. A bridge call cost ~133 ms while the SQL underneath
ran in ~1 ms — 16 ms ssh round trip, ~25 ms for blip-dispatch's Python start,
~20 ms for imsg's, 27 ms to open a 218 MB chat.db.

Worse, the shim fired a SECOND full ssh round trip before every call as a
connectivity probe — 41 ms, ~31% of the total — bought only to turn a
transport failure into a friendlier sentence. ssh reports that itself as 255,
and every caller (collector, thread, fetch, avatar, search, send-file,
contact-search) has always treated 255 exactly like 69. The probe is gone;
the real call carries the news and the shim translates 255 → 69, so the
documented contract is unchanged and ssh's own reason now reaches stderr,
which is more than the probe's one-line tail ever carried. It also retires
the trap that came with it: the probe HAD to be `ssh -n` or it ate stdin and
silently emptied --file-stdin payloads. Verified with a stub ssh that stdin
still reaches the remote command intact, that a transport failure exits 69,
and that any other exit code passes through untouched.

The push debounce came down from 250 ms to 60 ms. It is paid on every
received message and on every send — Messages writing the row is itself a
chat.db change, so the watch ping is what resolves the "Sending…" bubble —
and at 250 ms it was more than twice the cost of the 116 ms fetch it was
coalescing. A burst still costs one fetch. The post-send reload fell from
600 ms to 250 ms and is now the fall-back for a downed watcher rather than
the mechanism.

Measured, warm, medians: bridge call 132 → ~95 ms, open a conversation
223 → ~185 ms, poll 160 → ~118 ms, deep poll 707 → ~590 ms.

Two things the measurements talked us out of, both recorded in CLAUDE.md so
nobody re-tries them: replacing cmd_chats' 300-query N+1 with a single window
function is SLOWER (70 → 107 ms, the per-chat lookups are indexed), and
bundling the deep poll's three sequential calls into one would be undone by
the persistent channel coming next.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xxS9WCT7ifHhbDjYyGnHS
With the preflight probe gone a bridge call still paid ~90 ms before it read
a single row: the ssh round trip, blip-dispatch's Python start, imsg's own,
and 27 ms to open a 218 MB chat.db — for SQL that runs in about a
millisecond. Startup, not work, was the whole of the lag.

`imsg serve` answers many requests on one process with chat.db already open.
Requests are re-dispatched through build_parser(), the SAME parser the CLI
uses, so there is no second command table to drift into a second security
surface, and only read-only queries are reachable (SERVE_ALLOWED): attachment
and avatar stream binary and would park the channel behind a 100 MB photo,
watch blocks forever by design, serve would recurse. Framing is a
{"status","len"} line plus exactly len bytes — counted, not escaped, or a
122 KB payload would be re-encoded into a JSON string. A request carries its
stdin separately, so message text still never rides argv.

blip-bridged holds two of those channels open behind a unix socket in
XDG_RUNTIME_DIR. Two, not one: Blip refreshes the thread list and reloads the
open conversation in parallel on every push ping, and a single channel would
have queued the reload behind a 259 ms `chats` call — making the case it was
meant to help slower. The leader BarWidget runs it, exactly as it already
runs the push watcher, which is why Blip still needs no daemon of its own.

Nothing depends on it. No socket, no socat, a daemon that died mid-request, a
frame that will not parse, a short body — bridgeRun() falls through to the
ordinary one-shot ssh path on any fault at all. A caller that passes its own
runner never touches the socket, which is what keeps the existing suite
honest: those tests inject a runner and assert on real argv.

Measured end to end, medians: opening a conversation 223 → ~105 ms, a poll
160 → ~36 ms, a deep poll 707 → ~370 ms. On the channel itself, recent 150
went 100 → 18 ms and groups 145 → 26 ms; `chats` stays ~260 ms because it is
compute-bound (179 ms SQL, 104 ms name resolution), not startup-bound.

One trap worth the comment it now carries: `encoding: "buffer"` throws
ERR_UNKNOWN_ENCODING in Bun's spawnSync, the catch swallowed it, and the fast
path silently never ran while every call quietly took the slow one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xxS9WCT7ifHhbDjYyGnHS
With startup amortised by the serve channel, what was left in `chats` was
compute: 179 ms of SQL and 104 ms resolving names. The name half was
algorithmic. A handle with no exact last-ten key fell back to comparing it
against every saved number, so 298 conversations cost 60,112 _same_number
calls — 46 ms a request, and it grows with the address book.

Cards are bucketed by their last seven digits now. That is sound rather than
convenient: _same_number only matches when one national number is a SUFFIX of
the other with a floor of seven digits, so the last seven always agree, and a
card that does not share them cannot match at any calling code. The stripped
trunk-zero variant is bucketed too, since the rule tries the card both ways.
Verified against the old full scan over all 750 distinct handles in a real
chat.db: zero mismatches, largest bucket 2 cards instead of 442. That
comparison is the check to repeat if this is ever touched — CLAUDE.md now
says so, because these matching rules have been got wrong twice before.

name_for also memoises per handle, and group clusters and pins are memoised
across requests now that a process outlives one. They key on what they
actually derive from — the chat table's (count, max ROWID) and the pinning
plist's mtime — never on chat.db's mtime, which changes on every message and
would have cached nothing during exactly the busy minute that matters. The
Contacts index gains an invalidation for the same reason in reverse: a
one-shot run rebuilt it every time, while the serve channel would otherwise
answer with the names it started with until it died.

cmd_chats 292 → 215 ms; a rich thread load 72 → 52 ms (the cluster cache).
End to end against where this started: opening a conversation 223 → 83 ms,
a poll 160 → 39 ms, a deep poll 707 → 342 ms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xxS9WCT7ifHhbDjYyGnHS
The verdict predates the OS. Enumerating Messages' menu bar over System
Events on the gateway Mac turns up real menu items for three of the four
things that note rules out: Edit ▸ Tapback Message…, Edit ▸ Reply to
Message…, Edit ▸ Edit Last Message…, plus Send Later…. A menu item is
scriptable, which is precisely how imsg-read overturned the old "marking read
on the Mac is impossible" note. Typing indicators have no menu item and stay
out.

Nothing is claimed to work yet, and the note says so. All four read
enabled=false from the background, which by our own read-push finding is
evidence of nothing — AppKit validates menus against the ACTIVE app's
responder chain — so feasibility has to be retested with Messages frontmost.
The harder obstacles are recorded with it: the item acts on the SELECTED
message and selecting an arbitrary bubble from Linux is the unsolved part,
the "…" implies a picker to navigate for the emoji, Messages must come
forward so it steals focus, and a group still cannot be addressed at all.

Recording this because a stale "not possible" is the kind of note that stops
someone looking for years.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xxS9WCT7ifHhbDjYyGnHS
@zachwilke
zachwilke requested a review from nixfred as a code owner September 8, 2026 01:19
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-08T01:25:08.373492Z 586a4de PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 586a4def28

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread bridge/linux/blip-bridged
k, v = line.split("=", 1)
v = v.strip("\"'")
if k in conf:
conf[k] = v

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate config before constructing the remote command

When the dedicated key is absent or unreadable, _spawn() interpolates python and remote_bin into a remote shell command, but this new parser accepts those values verbatim instead of applying the shim's path validation. A malformed or hostile bridge.conf can therefore include shell metacharacters and execute arbitrary commands on the Mac through the general SSH path, reopening the bad-config threat the parsed shim was intended to close. Apply the shim's allowlist validation before starting any channel.

AGENTS.md reference: AGENTS.md:L26-L29

Useful? React with 👍 / 👎.

Comment thread collector.ts
*/
export function toUtcStamp(ts: string): string {
if (!ts || ts.includes("T")) return ts; // already UTC, or unset
const ms = Date.parse(ts.replace(" ", "T")); // naive → this machine's local

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Migrate legacy marks using the Mac timezone

On an upgrade where the Mac and Linux machines use different timezones, legacy state timestamps are Mac-local wall times, but this parses them in the Linux process's timezone. The resulting readMark can be shifted by the full offset: if shifted forward, new messages remain suppressed for hours; if shifted backward, previously read history is resurrected in the unread ledger. The migration tests only convert a source stamp in the same zone used by Date.parse, so they do not cover the cross-machine upgrade this change targets.

Useful? React with 👍 / 👎.

@nixfred

nixfred commented Sep 13, 2026

Copy link
Copy Markdown
Owner

Thanks for this, @zachwilke. All seven commits are worth having, and the UTC-on-the-wire change fixes an open ROADMAP item (Mac-local wall-clock stamps compared against the Linux clock).

It no longer merges: main has moved a long way (2.5.0 is being cut now), and GitHub reports conflicts in BlipView, collector and thread. As one +2182/−461 branch it's also hard to review safely, because the parts carry very different risk. Could you split it into separate PRs, each rebased on current main?

  1. UTC timestamps (688f2cb, with timezone.test.ts and test_wire_time.py). Highest value; please send this first.
  2. Animated GIFs (1feab93)
  3. Several files per message (20f8bc1)
  4. Bridge performance (0727e91, 5b8b01e, 934ee1f). This one needs the closest security look: blip-bridged holds a channel to the Mac open, which changes what the confined ssh key does in practice, so please say in the description how it keeps the forced-command gate and the ssh -n rules in CLAUDE.md.
  5. Docs (586a4de). Outbound tapbacks do not need SIP off — the Accessibility line imsg-read already crossed reaches them. In scope? #69 is already discussing outbound tapbacks, so this could go there.

Smaller PRs will also get reviewed and merged sooner.

@zachwilke

Copy link
Copy Markdown
Contributor Author

Fred — thanks for the split request. #47 is too large to review safely against current main (2.5.0 moved a lot; BlipView / collector / thread conflicted), so I rebased each slice onto main as its own PR:

  1. UTC timestamps (highest value, first): time: stamps cross the bridge as UTC; local time is a display concern #80
  2. Animated GIFs: gifs: an animated attachment arrives animated, and renders animated #81
  3. Several files per message: compose: a message can carry several files, not just the first one dropped #82
  4. Bridge performance (persistent channel + confined SSH key — please give this one a close security look; also includes the Codex P1 bridge.conf allowlist before _spawn()): perf: stop paying a startup toll; hold the Mac channel open #83
  5. Docs — tapbacks-out is half wrong on macOS 26, with a pointer to Outbound tapbacks do not need SIP off — the Accessibility line imsg-read already crossed reaches them. In scope? #69: docs: "tapbacks out is impossible" is half wrong on macOS 26 #84

This mega-PR can be closed in favor of those five. I am leaving that to you / Zach rather than closing it from here.

@zachwilke

Copy link
Copy Markdown
Contributor Author

@nixfred Closing this mega-PR in favor of the five smaller ones above — thanks again for the clear split guidance. Looking forward to your review on those (UTC first is ready whenever you are).

@zachwilke

Copy link
Copy Markdown
Contributor Author

Superseded by #80 #81 #82 #83 #84.

@zachwilke zachwilke closed this Sep 14, 2026
nixfred pushed a commit that referenced this pull request Sep 15, 2026
The live discussion is on that issue (AX-on-balloons, SIP on); nothing
from it is in-tree. Fred asked the docs slice of #47 to reference it.

Co-authored-by: zachwilke <zachwilke@users.noreply.github.com>
nixfred pushed a commit that referenced this pull request Oct 4, 2026
When the dedicated key is absent, _spawn() interpolates python and
remote_bin into a remote shell command. Codex's P1 on #47: a malformed
or hostile bridge.conf could inject metacharacters and run on the Mac
through the general ssh path, reopening the threat the parsed shim
closed (audit #7). The same regexes, before any channel starts; exit 78.

Co-authored-by: zachwilke <zachwilke@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants