跳转到主要内容

WRITING

Three Open-Source Contributions, In Retrospect: Raycast · WeChatTweak · Cardinal

July 17, 202613 min readTianli Zeng
Open SourceRetrospectivemacOSReverse EngineeringRaycast
Three Open-Source Contributions, In Retrospect: Raycast · WeChatTweak · Cardinal

Three Open-Source Contributions, In Retrospect: Raycast · WeChatTweak · Cardinal

In the first half of 2026 I sent one PR each to three upstream projects of completely different character:

  • Raycast DocKit — a document-formatting extension published to the Raycast Store (TypeScript + Python backend)
  • WeChatTweak WeChat 4.x anti-revoke — a macOS binary byte patch (Swift + ARM64 reverse engineering)
  • Cardinal persistent search history — a feature added to a 1610⭐ Rust+Tauri file-search app (React/TS)

The three span three different worlds — extension ecosystems, systems reverse engineering, desktop apps — with different languages, methods, and hard parts. This isn't a brag list; it's a retro: for each, the necessity (why it had to be done), the method (how exactly), and where the bar for merging actually sat.

Overview

Raycast DocKitWeChatTweak 4.x anti-revokeCardinal search history
Upstreamraycast/extensionssunnyyoung/WeChatTweakcardisoft/cardinal (1610⭐)
PR#26665 · ✅ merged#1039 · 🟡 open#222 · 🟡 open · CI all green
Language/stackTypeScript + Python CLISwift + ARM64 assembly REReact + TypeScript
Diff size+3499/-0 (11 files, mostly lock)+114/-10 (5 files)+517/-3 (22 files)
Source of necessityMy own heavy docx-formatting chores for institute reportsI wanted anti-revoke on WeChat 4.xI often search several keywords at once
Hard partPR-process hygiene + outlasting the stale-botPinpointing the patch site in an unsymbolicated dylibFraming it as a "supplement" not a duplicate, keeping a clean branch
ArtifactLive on the Storefork + reproducible byte patchself-compiled build already installed + PR

The three "bars for merging" in one line: Raycast is about process hygiene, WeChatTweak about reverse-engineering rigor, Cardinal about upstream etiquette. In none of them was the code itself the hardest part.


1. Raycast DocKit: the code was fine — twice it died on "a badly written PR"

PR #26665 · merged 2026-04-30 · Live on the Store

Necessity

Writing reports at the institute, my Word/PPT/Excel files are full of repetitive Chinese-typesetting grunt work: unifying mixed Chinese/English punctuation, pairing up quotation marks, standardizing unit symbols, unifying PPT fonts and table styles, converting tables between xlsx/csv/txt. I'd long since written a Python CLI for this (python3 -m dockit), but every time I had to open a terminal and type the command. DocKit wraps that CLI into three no-view Raycast commands that run against the files selected in Finder with a single hotkey:

  1. Format Word — pair quotes, convert English punctuation to Chinese, standardize unit symbols
  2. Convert Spreadsheet — xlsx ↔ csv ↔ txt (target format from a dropdown)
  3. Standardize PowerPoint — unify fonts, fix text formatting, set table styles

Select files → trigger the command → HUD shows the result → output written back to the same directory.

Method, and the real retro point

Technically it isn't complicated: three no-view commands in TS, with run-dockit.ts calling the local Python backend. What's actually worth recording is that this was already my third time submitting an extension to raycast/extensions — the first two both stalled and got closed.

The same person submitting an extension to raycast/extensions three times: the first two used an empty PR template, no demo, no follow-up, and got worn down to auto-close by the stale-bot; DocKit filled out the PR body, recorded a demo, replied to every review comment, and bumped past the stale-bot — and that's what got it merged and published
The difference wasn't the code — it was PR hygiene and the patience to follow up

The lesson is blunt: the first two (ts2, quick-open-app) may well have had fine code; the cause of death was submitting the PR on a bare template — description blank, no demo recorded, no replies to review, and then worn down to auto-close by the stale-bot. DocKit did the opposite, the whole package: filled out the PR body (command table + How it works + install steps + live demo + an explicit screencast description), passed ray lint/tsc/CI, and — crucially — followed up actively: replied to every review comment, and even dropped a keep-alive comment right before the stale-bot could close it, until a maintainer approved.

For Raycast, getting merged relies on PR hygiene plus the patience to outlast the stale-bot — as much as on the code itself. Of the three contributions, this was the most "non-technical" lesson.


2. WeChatTweak WeChat 4.x anti-revoke: flipping one branch instruction inside an unsymbolicated dylib

PR #1039 · open · fork zengtianli/WeChatTweak

Necessity

Upstream WeChatTweak only covered up to WeChat 3.8.x — the revoke logic lived in the main binary Contents/MacOS/WeChat. WeChat 4.x moved that logic into Contents/Resources/wechat.dylib (a stripped C++ core). The result: the tool still patched the main binary at hardcoded paths, hit no revoke code at all on 4.x, and reported unsupported for every 4.x build number. To get anti-revoke on 4.x, you have to go into the dylib and find the patch site yourself.

Method: flip 4 bytes — and verify the bytes before flipping

The core is a single sentence — turn the conditional branch cbz w0, SKIP at the parseRevokeXML entry into an unconditional branch b SKIP. The revoke instruction still gets received and parsed, but the code that deletes the message can never be reached:

How the anti-revoke patch works: in wechat.dylib, flip the cbz w0 conditional branch at the parseRevokeXML entry (E00F0034) into a b unconditional branch (7F000014) — the revoke instruction is still received and parsed, but the message-deletion code can never be reached
Flip only 4 bytes, in place and same-length; verify the original bytes before writing; refuse to write on the wrong version

The rigor shows up in a few places — this is the dividing line between a reverse-engineering patch tool and "just tweak something":

  • In-place, same-length replacement: cbz and b are both 4-byte fixed length with the same target offset, so it only flips E00F00347F000014 and doesn't change the binary layout.
  • Address located by geometric signature, not by symbol (symbols are stripped): the stp prologue at the parseRevokeXML entry + the cbz w0 at entry+0x270 + the str x0 at entry+0xA04, combined, are a unique hit across the whole arm64 slice — then verified byte by byte by disassembling.
  • Verify the original bytes before writing (the expected field): only write if the target really is E00F0034; on the wrong WeChat version it reports expectedMismatch and refuses to write, so it won't blindly write and break WeChat.
  • Re-signing order: ad-hoc sign the modified dylib alone first, then --deep sign the whole App, to avoid a Code Signature Invalid when execution reaches the modified code page.
  • Verification can only be done on a real device: get someone to actually send a message and then revoke it — it only counts if the revoke is really blocked. With symbols stripped, static analysis can't confirm it.

Later I also implemented a "keep the tip" variant (--variant keeptip): instead of blocking parsing, it zeroes out newmsgid (str x0str xzr) so the deletion misses while the tip is still inserted. Along the way I corrected a directional mistake — early on I thought I had to NOP the downstream deletion call (which is on the virtual-dispatch receive side and hard to locate statically, so it was shelved), but later realized there was no need to find the deletion call at all: zeroing it out at the source is enough. Write down the wrong turns too — that's the value of a retro.

Bonus output: helping the community make sense of the "three walls"

In issue #1038 a bunch of "my version is different from yours" users were stuck, but they were actually stuck on three different problems — all muddled together under the same Unsupported version line. I drew it as one chain:

The three walls users hit in sequence in issue #1038: wall 1, the version wall (build number not in config); wall 2, the path wall (patch reads remote master and can't see a locally added entry); wall 3, the write wall (macOS 14+ App Management/TCC blocks the write) — the first two report the same "Unsupported" line but have different root causes
The most valuable contribution to an open-source project is sometimes not code — it's diagnosing a messy problem cleanly
Wall 1 · version wallWall 2 · path wallWall 3 · write wall
What it reportsUnsupported versionUnsupported version (same error, different root cause)You don't have permission to save "wechat.dylib"
Root causeBuild number's addresses all changed, not in configThe patch reads remote master by default and can't see the version you added locallymacOS 14+ App Management/TCC blocks modifying a signed app; sudo doesn't help (TCC checks the initiating process, not root)
Solved?locate_revoke.py locates it automatically✅ changed the default to read config local-first⚠️ root cause identified; the fix = grant the terminal "App Management" permission — not a code problem

Walls 1 and 2 are two different causes of the same error line, the easiest to conflate; wall 3 is another layer (write permission), unrelated to version or config path. Splitting them apart and drawing them on one chain is far more useful than replying "you probably mis-copied the addr" one by one — the most valuable contribution to an open-source project is sometimes not code, but diagnosing a messy problem cleanly.


3. Cardinal search history: how to make a change "worthy" of being merged

PR #222 · open · CI all green · MERGEABLE

Necessity

Cardinal is an "Everything-style" instant filename-search app for macOS (Rust+Tauri, self-built index; on my own machine it indexed 6.43 million files). I often need to look up several things at once, but its search history lived only in memory — gone when you close the app, and only reachable via the ↑/↓ keys, with no clickable UI. What I wanted was persistence plus a clickable dropdown panel.

Method: the feature itself + making it "mergeable"

The feature isn't technically demanding: a useRecentSearches hook that writes queries into localStorage (dedupe-to-top, cap of 50), and a clock button whose dropdown re-runs a query on click. The interesting design decision is when to record — not only on Enter, but also when you open a result (a search that led to "opening a file" is the search worth keeping):

When Cardinal's search history records: submitting with Enter, or opening a result via double-click/Cmd+O/right-click, all write to localStorage (dedupe-to-top, cap of 50); editing the query without submitting or opening records nothing
A search that led to "opening a file" is the search worth keeping

But for a PR to a 1610⭐ repo, the real work is in making the change worthy of being merged:

  • Check for prior work first, then frame it as a "supplement": upstream PR #31 (merged) had added in-memory ↑/↓ history navigation. Mine is persistence + a clickable panel — a supplement, not a replacement — and the PR text spells out this relationship and thanks #31 (using a separate hook file, not touching #31's code at all).
  • Self-review and fix nits before opening the PR: an adversarial round of code review caught the dropdown misusing the role="listbox"/"option" ARIA anti-pattern — in a repo that cares about accessibility, the maintainer would very likely ask for a change. So before submitting I changed it to plain button semantics + aria-haspopup, leaving no obvious handle.
  • Keep the PR branch clean: only the 22 files of the feature itself, excluding the CLAUDE.md/.claude I keep for my own fork — upstream shouldn't see those.
  • Pass every CI gate locally before pushing: format:check / typecheck / vitest (278 all pass) / build all green before push. Result: all 10 upstream CI checks green on the first try.

Whether upstream merges you depends on whether you've thought through everything the maintainer would have to — checking prior work, thanking those who came before, fixing foreseeable review comments, keeping the branch clean, CI green. Etiquette is about minimizing the cost of reviewing you.


Cross-project retro: three bars for "upstream merge"

Having gone through all three, my biggest takeaway isn't a piece of code — it's seeing clearly where "getting merged" actually sticks in different ecosystems:

EcosystemThe real barMy response
Raycast (extension Store)Process hygiene + patience — the code might be fine, but an empty PR body + no follow-up = worn to death by the stale-botFill out the PR, record a demo, reply to every review comment, bump past the stale-bot
WeChatTweak (systems RE)Rigor — one wrong byte in an unsymbolicated binary breaks the appUniquely locate by geometric signature + verify bytes before writing + verify on a real device + honestly flag the unsolved parts
Cardinal (mature app)Upstream etiquette — why should a maintainer spend time reviewing youCheck prior work and frame as a supplement, thank predecessors, fix nits before submitting, clean branch, CI all green

A few general points:

  1. Every "necessity" came from my own real workflow — report typesetting, wanting anti-revoke, frequently searching multiple terms. I didn't hunt for projects to farm contributions; I had a pain point first and turned it into a public output along the way. That's what makes it sustainable.
  2. Honestly flagging "the parts I didn't solve" is worth more than pretending to be omnipotent — in WeChatTweak I wrote plainly that group chats have no tip and that the write wall is a permission problem, not a code one; in Cardinal I said plainly that strings on the app binary can't catch the frontend, and that a11y verification relies on "source → build → CI" rather than grep. A retro has to hold up when I re-read it myself.
  3. "Making others willing to merge" is a craft independent of writing code — it holds across all three ecosystems, and it's exactly what the first two failed Raycast PRs taught me.

What this means for my career

All three are publicly verifiable outputs at the intersection of domain + code (landing squarely on my core positioning of "hydro domain + code + AI applications") — far harder than a résumé line saying "familiar with XX," because the PR links, the Store listing, and the merge record can all be dropped on the table directly. The reverse-engineering one especially demonstrates "can chew on hard problems." The forward-looking anchor: once Cardinal #222 is merged upstream, it's a merged contribution to a 1610⭐ MIT project — worth staying on top of until it lands.


Source material: the three PRs themselves + ~/Apps/WeChatTweak/docs/{anti-revoke-patch,user-blockers}.md (reverse-engineering notes) + the self-compiled Cardinal fork (~/Dev/cardinal).