home
$ cat journal/*.md | less

journal

# deep-dives, benchmarks, and notes from research

Portfolio Inbox: Twelve Bugs Found by Execution, Not by Reading

Hardening the Portfolio contact inbox (Pages Function → private GitHub repo → local mirror + append-only archive) surfaced 12 defects, of which 10 were found only by running the code, not by reading it. Two mattered most: (a) String(undefined) is the literal string "undefined", so a submission that omitted a required field passed the falsy-value guard and was permanently archived under that name; (b) the archive's markdown block parser could be made to archive a forged message by a visitor, because the sanitizer escaped one delimiter character but left line structure intact, and my first regex accidentally blocked the obvious attack while leaving a precision-variant wide open. A recurring second class was wrong diagnosis — an edge-rule rejection, a missing JSON field, and an absent-but-tolerated file were each reported as credential failures. Headline lesson: a test that fails because the test is wrong is still information — my one bad assertion is what exposed the injection hole.

portfolio cloudflare-pages workers injection fence-parsing sanitization preflight health-check false-positive waf testing

Redaction note (2026-09-28). The first public version of this entry included operational specifics that doubled as attack aids. They have been removed, and this copy deliberately does not describe which ones — an enumeration would itself confirm the shape of the attack surface. The bugs, the counts, and the lessons stand on their own; none of them needed the recipes. The local session record retains the full detail.

2. Background & Problem Statement

The contact form does not email. The Function validates, then performs a full PUT rewrite of a markdown file in a private GitHub repo, prepending the new block. A separate maintenance endpoint truncates that file back to an empty placeholder. So the remote file is a snapshot, not a log — a dated history proves it: eight messages exist only in git history, absent from the file.

Three properties had to hold simultaneously:

  1. Loss-free. A remote reset must never destroy a message.
  2. No false negatives. A new message must be detected exactly once.
  3. No false positives. A forged or malformed payload must never be archived or reported as a real message.

Plus a credential preflight, because the failure mode nobody sees is the Platform secret dying silently: visitors get an error and simply give up.

3. Environment / Setup

ComponentDetail
SiteCloudflare Pages (project hashbyte), Functions on the Workers runtime
Runtime constraintsV8 isolate, fetch only — no shell, no ssh, no binaries
Storeone markdown file in a private GitHub repo, default branch main
Write credentiala Pages secret (bearer token) held by the Function
Read credentialgit over SSH, run locally
Read fallbackan authenticated GitHub CLI token on machines with no SSH agent
Gate credentiala second Pages secret gating the two maintenance endpoints
ToolchainNode v26.8.2, Python 3.14.7, gh 2.100.0
Test harness5 suites, 92 assertions (12 / 3 / 22 / 27 / 28)

3.1 SSH vs token — the question that clarified the whole design

"I connect to the repo using SSH — why would I need a token?"

Two unrelated authentication systems:

authenticatesusable for
SSH keythe git protocolclone / fetch / push only
OAuth / PAT tokenthe REST APICLI tooling, and the Function

SSH keys are meaningless to the REST API — an unauthenticated request for a private repo returns 404, not 403, precisely so the repo's existence isn't confirmed. But the token is not for us; it is for the platform host. The Workers runtime cannot do SSH under any configuration, and handing the host an SSH key to write one repo would hand it write access to every repo that key can reach. A token scoped to a single repo is strictly safer. The token is the correct design, not a workaround.

Also established: a passphrase-protected key with no running agent fails Permission denied (publickey) in cron / launchd / CI, so unattended reads need the keychain unlocked first.

4. Investigation / Method

Bugs were classified by how they were found, because that is the transferable part:

  • E = found by executing code (unit/integration test or live probe)
  • R = found by re-reading before deploy
#BugClassFound by
1String(undefined) → "undefined", passes a falsy guardinjection/validationE (live probe)
2Fence-unaware parser archived a forged blockinjectionE (test)
3Edge rule returned 403 on the default client UA, reported as a credential faultwrong diagnosisE (live)
4CLI auth status collapsed a 401 into "not logged in"wrong diagnosisE (test)
5Scope regex captured only the first of several scopeswrong diagnosisE (live)
6Absent field treated as false → "secret missing" for a read-only tokenwrong diagnosisE (test)
7Absent target file reported unhealthy (false alarm)wrong diagnosisE (test)
8GET unusable as a liveness signal — Pages serves the static sitewrong diagnosisE (live)
9Non-JSON 200 → confusing unexpected HTTP 200wrong diagnosisE (test)
10Error-body read inside the exception handler can itself raisecrashE (test)
11Within-batch duplicates archived twice; across-sync collapsed oncelogicE (test)
12Maintenance endpoint's two 500s swapped during refactorregressionR

5. Experiments & Results — with Evidence

5.1 Bug 1 — String(undefined) is not empty

String(undefined) === "undefined" and String(null) === "null", so every sanitizer returned a truthy string for an absent field. The combined validator was accidentally safe only because that literal fails both the email and phone regexes.

$ node -e "import('./functions/api/_shared.js').then(m=>{
              console.log('sanitizeName(undefined)    =', JSON.stringify(m.sanitizeName(undefined)));
              console.log('validateContact(undefined) =', JSON.stringify(m.validateContact(undefined))); })"
          sanitizeName(undefined)    = "undefined"
          validateContact(undefined) = null

          # Post with the name field OMITTED, contact + message valid:
          #   name    -> "undefined"  (truthy, so the `!name` guard does NOT fire)
          #   contact -> "real@example.com"
          #   message -> "hello"
          #   => ACCEPTED, archived with name "undefined"

Surfaced by noticing the live endpoint's error text. An empty post returned the contact error rather than the name error — proof the name guard had not fired:

$ curl -s -X POST https://hashbyte.pages.dev/api/contact \
              -H 'content-type: application/json' -d '{}'
          {"error":"that email/phone does not look right"}     # expected: "a proper name is required"

Fix: a coercion helper mapping undefined/null → '' in all three sanitizers, so the required-field guards fire. 13 assertions; normal values unchanged.

This matters because the archive is append-only: a garbage entry can no longer be cleanly removed later.

5.2 Bug 2 — forged block injection (the near-miss)

Block identity is taken from a level-2 markdown heading carrying an ISO timestamp, matching what the writer appends. The sanitizer strips control characters and escapes the code-fence delimiter, but leaves line structure and heading markers untouched, so a visitor controls arbitrary lines inside the fence.

The first cut identified blocks by regex alone. It appeared to work, because the natural attack string — a far-future timestamp with no sub-second component — failed to match:

attacker body line:  heading + far-future timestamp, no sub-second part
                               -> does NOT match (the pattern requires them)  -> "safe"

That was false confidence. Adding the sub-second part made it match perfectly:

$ python3 -c "...parse_blocks(body)..."
          blocks parsed: 2 (expected 1)
           - the genuine block
           - a heading the visitor invented   <-- attacker-controlled, archived as real

Fix: fence-aware parsing — a heading only opens a block when not inside the message fence. The payload still round-trips verbatim inside the genuine message. The fence is reliable precisely because its delimiter is escaped, so an unescaped delimiter can only be the block's own.

How it was found — the part worth keeping. My first regression test asserted the archive must not contain the string Forged. It failed. The obvious read was "my parser is broken"; the correct read was "my assertion is wrong — Forged legitimately appears inside the code fence as payload". Fixing the bad assertion prompted testing the precision variant, which is where the real hole was. A failing test is sometimes the test's fault, but it is always information.

5.3 Bug 3 — the edge rule impersonates a credential fault

An edge rule refuses the default urllib user-agent outright, so the first version of the preflight reported that rejection as a broken token — sending the user to rotate a perfectly valid credential. It never reached the Function, so it learned nothing about the token at all.

# pre-fix: default client UA  -> 403  (edge refused; secret never tested)
          # post-fix: app-specific UA    -> 400  (reached the Function's own validation)

Fixed by pinning an explicit user-agent, and by reporting a 403 as an edge block, never a token fault. Asserted as a permanent regression guard.

5.4 Bug 8 — GET cannot detect a missing Function

GET  /api/health (undeployed) -> HTTP 200  type=text/html   <-- the static site
          POST /api/health (undeployed) -> HTTP 405                   <-- clean signal
          POST /api/contact (deployed)  -> HTTP 400  type=application/json

A GET is unusable as a liveness signal: Pages serves the static asset for an undeployed Function path, and 200 text/html is indistinguishable from a real answer. A POST to a missing Function is a clean 405. The client therefore POSTs, and on 405 — or a 200 that is not JSON — falls back to the weaker submission probe and reports validity UNPROVEN rather than quietly passing.

5.5 Bugs 6 & 7 — two false diagnoses in the health check

  • The remedy dispatch tested "configured is falsy" first. A response that merely omitted the field satisfied that test → "secret missing" reported for a token that was merely read-only. Fixed: dispatch on the failure text first, and compare with is False.
  • The target file returning 404 was scored unhealthy. It is not a fault: the read path maps 404 to an empty placeholder and the write path recreates the file, so writing still works. Now reported as file: "absent" with ok: true.

5.6 Verifying write capability without writing

The API has no "can I write here?" call, but the authenticated repo payload carries the token holder's own permissions. So a push permission flag proves a write would be accepted — two GETs, zero mutations:

--- it must never write ---
          PASS  zero non-GET calls
          PASS  exactly 2 GETs (repo + contents)
          --- cache: cannot be used to burn the token's rate limit ---
          PASS  25 calls -> still only 2 API calls
          PASS  cached responses are marked

Hardened for a public site: gated behind the shared secret; booleans and status codes only, never the token and never an upstream body (a raw pass-through could leak token detail inside an error payload); 60s per-isolate cache.

5.7 Bug 12 — caught by re-reading, not by a test

Refactoring the maintenance endpoint onto a shared guard helper silently swapped the order of its two 500s. Only reachable when both secrets are unset, so no happy-path test would notice. Caught by diffing against the original before deploying, and preserved exactly by splitting the "is it configured" check into its own helper.

6. Conclusions

  1. Execution finds bugs that reading does not. 10 of 12 defects were invisible to inspection. Both critical ones (String(undefined), forged-header injection) look correct in a code review.
  2. A passing test is not a safe test. The precision requirement in the block regex accidentally blocked the obvious attack and nearly shipped. The security property needed was "a heading inside a fence is never a heading" — expressed as fence tracking, not as a stricter regex.
  3. A wrong diagnosis is worse than no diagnosis. Six bugs mis-pointed the user at the credential when the fault was elsewhere (edge rule, absent field, tolerated 404, missing Function, static fallthrough). Each is now asserted.
  4. Probe the thing that actually fails. A CLI auth status call also exits non-zero on a revoked token, so the API probe must run first and status is used only to enrich a pass.
  5. When a fallback is weaker, say so in the output. validity UNPROVEN is honest; a green check that only proved presence would have been a lie.
  6. Treat append-only archives as write-once. A malformed entry can never be cleanly removed, so validation must reject at ingest rather than clean later.
  7. Publishing a postmortem is a second, separate disclosure. Operational specifics — bypass strings, secret names, file locations, parser formats — are attack aids even when the write-up is about fixing them. Redact the recipes, keep the reasoning.

7. Final Configuration

ConcernDecision
Local read transportgit over SSH (no token); --transport api for keyless machines
Working copydata files are siblings of the clone, so no git reset/clean can clobber the archive
Mirrorwhatever git checked out; hand edits self-correct on next pull
Newnesssha256 of the whole block, not position or commit
Write-ability checkauthenticated repo metadata → push permission; zero writes
Absent target fileok: true — the write path recreates it
Absent fieldcoerced to '' so required-field guards fire
Health livenessPOST (not GET); 405 ⇒ not deployed ⇒ fall back and say UNPROVEN
Sensitive endpointsgated on a shared secret; 60s cache; booleans only
Auto-renewalnot possible — a platform secret is set by hand and redeployed. An app-installation token is the only self-renewing design.

Key files

  • functions/api/health.js — read-only token health check
  • functions/api/_shared.js — coercion helper, fence-safe contract, shared guard
  • scripts/inbox/sync_inbox.py — SSH/API transport, hash-dedupe archive, preflight
  • inbox/ — mirror, archive, and a git working copy (gitignored: visitor PII)

Deployed 2026-09-28 (all three Functions, plus the coercion fix):

npx wrangler pages deploy . --project-name hashbyte --commit-dirty=true

8. Appendix — Reproduction

# 1. String(undefined) coercion  (bug 1)
          cd /Volumes/Xbyte/Projects/Portfolio/functions/api
          node --input-type=module -e "
          import { sanitizeName, validateContact } from './_shared.js';
          console.log(JSON.stringify(sanitizeName(undefined)), JSON.stringify(validateContact(undefined)));"
          # pre-fix: "undefined" null      post-fix: "" null

          # 2. live endpoint before the fix showed the un-guarded name
          curl -s -X POST https://hashbyte.pages.dev/api/contact \
            -H 'content-type: application/json' -d '{}'
          # {"error":"that email/phone does not look right"}   <- should be "a proper name is required"

          # 3. SSH unusable non-interactively
          ssh-add -l                       # The agent has no identities.
          #   -> Permission denied (publickey); unlock the keychain, or use --transport api

          # 4. full suites (92 assertions)
          cd /Volumes/Xbyte/Projects/Portfolio
          python3 /tmp/xgcode/test_sync.py && python3 /tmp/xgcode/test_check.py \
            && python3 /tmp/xgcode/test_git.py && python3 /tmp/xgcode/test_health_client.py \
            && node /tmp/xgcode/test_health.mjs

          # 5. preflight, both transports
          ./scripts/inbox/pull.sh --check-only                  # ssh
          ./scripts/inbox/pull.sh --transport api --check-only # CLI token

On omissions. Some details present in the working notes were left out of this public copy. Which details is itself information, so they are not listed here; the local session record retains them.

XGcode: Flooring the Reported Version at the Fork Base for the Console Free Tier

While running a live Console-authenticated task, the opencode Console free tier rejected the XGcode client. The Console floors the reported client version at 1.18.0 on its free tier; XGcode's preview/dev-channel builds bake Script.version (which resolves to 0.0.0-*) into the binary's OPENCODE_VERSION define, and the Console reads that as below the floor. Fix: floor the reported version at the fork's base release (1.18.0) for preview/dev channels only, at build time, via a shared Script.base getter — keeping Script.version untouched for upstream release/upgrade detection.

xgcode fork console free-tier version-floor preview dev-channel bun build define gateway auth

2. Environment / Setup

ComponentDetail
Mac (this host)/Volumes/Xbyte/Projects/XGcode (fork user)
XGcodefork of opencode, base release 1.18.0
ToolchainBun 1.4.x; bun run --cwd packages/opencode build / build-node
Version sourcepackages/script/src/index.ts → Script.version / Script.channel / Script.preview
Binary definespackages/opencode/script/build.ts + build-node.ts → bake OPENCODE_VERSION, OPENCODE_CHANNEL
Console floorfree tier accepts reported version ≥ 1.18.0 (server-side, unpatchable)
Failure modefree tier rejects preview-channel builds reporting below 1.18.0

3. Problem

  1. Console free tier is a server-side version gate. It floors the reported client version at 1.18.0; the rejection string ("1.18.0 or newer is required") is not in the repo (verified git grep).
  2. Preview/dev builds report 0.0.0-*. packages/script/src/index.ts resolves Script.version to 0.0.0-${CHANNEL}-${timestamp} on any channel ≠ latest (Script.preview), which the Console reads as < 1.18.0 → free tier auth fails.
  3. The version is baked in at build time, not read at runtime. Both build scripts define it statically (build-node.ts:29, build.ts:194), so any fix must happen at build time.

4. Investigation / Method

4.1 Where the version flows

  • Script.version → 0.0.0-* on preview/dev channels.
  • build.ts/build-node.ts bake Script.version into OPENCODE_VERSION define.
  • The Console reads the baked version; free tier rejects < 1.18.0.

4.2 The wrong (band-aid) approach

First attempt: pass an env override at build time (OPENCODE_VERSION=1.18.0 bun run build-node.ts). This works for the smoke test but is not durable — it must be remembered per build and doesn't live in the fork.

4.3 The durable fix

Make the floor part of the shared Script contract, used by both build scripts on preview/dev channels:

  1. In packages/script/src/index.ts: add const BASE_VERSION = "1.18.0" and a Script.base getter (floors the reported version to the fork base).
  2. In both build-node.ts and build.ts: floor only for preview:
OPENCODE_VERSION: `'${Script.preview ? Script.base : Script.version}'`,
  • latest channel → Script.version (real upstream) — unchanged.
  • preview/dev channel → Script.base (= 1.18.0) — Console free tier satisfied.

This matches the fork's rule: keep upstream contract/version identity (Script.version) for compat; floor the reported version at the fork base.

5. Results — with Evidence

5.1 Build + smoke (no env override)

A preview/dev-channel build with the fix, run with no OPENCODE_VERSION env var:

$ bun run script/build.ts --single --skip-install --skip-embed-web-ui
          ...
          Smoke test passed: 1.18.0

verification, and the loop actually ran (enumerate matching on-disk files after a bulk replace). The reliable backstop here: single, sequential, self-labeled commands (one grep/one read at a time) + authoritative git status/git diff before each edit. Parallel reads of near-identical files in this session kept returning each other's output — trust the git backstop.

6. Conclusions

  1. The Console free tier is a server-side version gate (floor at 1.18.0); the fork cannot patch the client's displayed version after the fact, so the floor must be baked in at build time.
  2. Floor the reported version, not the real one. Keep Script.version authoritative for upstream release/upgrade detection; Script.base is the fork base used to floor what the Console sees on preview/dev channels.
  3. Keep Script.preview ? Script.base : Script.version in both build scripts so the latest channel and preview/dev channels each report correctly.

7. Final Configuration (durable change)

ConcernValue
Reported to Console (preview/dev)1.18.0 (Script.base)
Reported to Console (latest)Script.version (upstream-derived, unchanged)
Release/upgrade detectionScript.version — untouched
Where flooredScript.preview ? Script.base : Script.version in build.ts + build-node.ts

Key files:

  • packages/script/src/index.ts — BASE_VERSION = "1.18.0" + Script.base getter
  • packages/opencode/script/build-node.ts — flooring define
  • packages/opencode/script/build.ts — flooring define

xgcode --version (any preview/dev build) → 1.18.0; Console free tier accepts.

From IRC Code-Theft War to a Modern Windows Sentinel

CopyGuard started around 2004 as an mIRC script war: a custom mSL + DLL copy guard that let people read code without taking it — freeze the remote editor, poison the clipboard, auto-respawn on exit, auth-gated quit. Two decades later the same instinct rebuilt it as 2.0 after an idle PC was eating 6–12% CPU for no visible reason: paste blocker, clipboard lock, autostart watchdog, and two processes that refuse to stay dead.

mircmslirc clipboardpaste-blocker.netwindows

the 2004 trick

  • Custom IRC patch — remote editor opened the script, then froze; visible, but no scrolling
  • DLL controls the clipboard while IRC.exe runs — copy anything, paste nowhere
  • Auto-respawn on exit via on *:EXIT — killing the client was a dead end
  • Only a proper, authenticated exit stopped it

the 2026 rebuild

  • Paste blocker swallows Ctrl+V/Shift+Insert for listed apps via keyboard hook
  • Clipboard lock caches text in RAM, empties clipboard on focus in, restores on focus out
  • Persistence sentinel watches every autostart location, alerts and optionally reverts changes
  • Two processes revive each other — stop.flag + authenticated cgctl is the owner's way out

the twist

  • Built to find what eats idle CPU — it found itself: DiskSentinel was hammering a failed USN journal read every 5s
  • Policy fix dropped CopyGuard to 0.4% CPU; the real background eater was ASUS bloatware

pgvector 768-dim HNSW Retrieval + PyRIT Pentester Bridge

Migrated XG's memory from opaque JSONB arrays to native pgvector halfvec(768) columns with HNSW cosine indexes. Integrated PyRIT as an allowlist-gated pentest_scan MCP tool with a verified mock-sandbox harness. Found a pre-existing _log NameError that silently broke every memory write.

pgvectorhalfvechnsw ragpyritmcppostgres

headlines

  • jsonb → halfvec is only possible through text — safe migration is dual-write
  • PyRIT's default scorer wants OpenAI-compatible chat — adversarial chats use distinct JSON schemas
  • HTTP target prompt must live in the POST body, not headers
  • Blocking issue: swarm worker synthesizes "completed" results with zero artifacts

environment

  • Mac host: user void, workspace /Volumes/Xbyte/Projects/XG/
  • PostgreSQL 16 via Homebrew, pgvector source-built
  • Embedder: nomic-embed-text → 768-dim via ollama

built

  • Migration: scripts/migrate_embeddings.py
  • PyRIT pentest MCP tool: tools/pentest_scan.py
  • Mock sandbox harness + regression tests — all green

KeyCastr v0.11.1 upstream merge done entirely without Xcode (CLT + clang + shipped .app zip)

A fork of KeyCastr (at upstream v0.10.5) needed to be brought forward to upstream v0.11.1 (40 commits) while preserving a heavily customised, uncommitted rewrite of KCDefaultVisualizer.h/m (a Core Animation keycap visualizer) — and the whole task had to be done on a Mac with no Xcode installed, only Command Line Tools. The goal was to prove the fork state merges cleanly and the custom plugin still compiles, links, and loads at runtime. It worked end-to-end. The key trick: instead of building the whole app with xcodebuild (impossible — CLT errors with "requires Xcode"), rebuild only the one changed plugin with bare clang, and use the officially shipped prebuilt keycastr/bin/KeyCastr.app.zip as the dependency + bundle skeleton (it already contains KCVisualizer.framework, all plugins, nibs, and the source tree's compiled state). The custom KCDefaultVisualizer.kcplugin was compiled universal (arm64+x86_64), linked against the framework from inside the unzipped app (-F <app>/Contents/Frameworks), swapped into PlugIns/, the app ad-hoc re-signed (removing hardened runtime so the untrusted plugin loads), and verified by an actual headless launch (lsof shows the plugin binary mapped into the process, zero errors in the log). Headline lessons: (1) git merge FETCH_HEAD silently lies ("Already up to date") — always merge the explicit commit SHA. (2) Xcode-less ARC/MRC correctness comes straight out of project.pbxproj as fine-grained -fobjc-arc/-fno-objc-arc flags — the framework is MRC, a few files are per-file ARC, and three visualizer targets are ARC at the target level. (3) A zip made on macOS carries ._* AppleDouble stubs that unzip materializes inside the bundle root → they break codesign with "unsealed contents present in the bundle root"; delete them before re-signing. (4) The stock app is signed with hardened runtime, so an ad-hoc plugin (Team ID "not set") would be rejected by library validation on load — the in-place re-sign must produce an ad-hoc signature without the runtime option.

keycastr objective-c no-xcode command-line-tools clang codesign ad-hoc-signing plugin-bundle arc-mrc git-merge apple-double hardened-runtime kcplugin macos

2. Background & Problem Statement

  1. The fork is behind upstream and has local custom code. keycastr fork main was on v0.10.5 (58133a7); upstream had since reached v0.11.1 (1e552a1, 40 commits ahead). The user rewrote the default visualizer (KCDefaultVisualizer.h/m) into a keycap-based Core Animation renderer rather than the upstream bezel/text style — these changes were never committed and live only in the working tree.
  2. The merge must not clobber the rewrite. Standard "merge upstream then fix conflicts" is fine if upstream never touched those two files. Verification that the custom files survive the fast-forward is the whole point.
  3. No Xcode, no xcodebuild. The box has /Library/Developer/CommandLineTools only. xcodebuild fails with "requires Xcode" (CLT alone cannot run it; only xcrun, clang, ld, lipo, codesign, otool, nm are available). So there was no project build → the "does it still build?" question had to be answered by hand-rolling the compile/link.
  4. Mixed ARC/MRC in one source tree. KeyCastr's KCVisualizer.framework is MRC (manual reference counting) while three visualizer plugins and a handful of app files are ARC. Getting this wrong silently produces retain/release errors at runtime, not compile time. The truth lives in project.pbxproj in three different places (target-level CLANG_ENABLE_OBJC_ARC, per-file -fobjc-arc COMPILER_FLAGS, and the framework target defaulting to no ARC).
  5. Runtime validation needed without Accessibility permissions. The app needs Input Monitoring to actually show keystrokes, which this machine can't grant headlessly. "It works" therefore has to be proven at the process level: plugin bundle enumerated + loaded by NSBundle, no crash, no error output.

3. Environment / Setup

ComponentDetail
HostmacOS 26.x aarch64 (Apple Silicon), user void
XcodeNOT installed — xcodebuild fails with "requires Xcode"
ToolchainCommand Line Tools at /Library/Developer/CommandLineTools (clang/ld/lipo/otool/nm/codesign from here)
SDK/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk (SDK version 26.2; deployment target used throughout: macOS 11.0)
Repo/Users/void/Projects/keycastr (git, branch main)
Fork base58133a7 (v0.10.5)
Upstream tip1e552a1 (v0.11.1), fetched via remote https://github.com/keycastr/keycastr
Merge size40 commits (`git log --oneline 58133a7..HEAD \wc -l` = 40)
Submoduleskeycastr/Externals/ShortcutRecorder @ c86ce0f… (Kentzo fork), keycastr/Externals/Sparkle @ ac2def288… (Sparkle 2.x)
Build cache dir/var/folders/y9/gsb6gx0x7bn1d1r2370sknc40000gp/T/xgcode/keycastr-build/
Prebuilt basekeycastr/bin/KeyCastr.app.zip — the official v0.11.1 app containing KCVisualizer.framework, Sparkle.framework, ShortcutRecorder.framework, and all .kcplugin bundles
Repo deliverablebuild/KeyCastr.app (repo-relative build/, gitignored)
Final git state1e552a1 (v0.11.1) on main, custom KCDefaultVisualizer.{h,m} still uncommitted M working-tree changes

3.1 ARC/MRC map (exact, from KeyCastr.xcodeproj/project.pbxproj)

GroupMemory modelHow expressed in the pbxproj
KCVisualizer.framework core (KCVisualizer.m, KCColorValueTransformer.m, NSUserDefaults+Utility.m, KCUserDefaultsMigration.m, KCKeystroke.m, KCMouseEvent.m, KCKeycastrEvent.m)MRCtarget-level ARC off; per-file -fno-objc-arc not even needed (default is MRC)
SvelteVisualizer.m, main.m, KCPrefsWindowController.mMRCno ARC anywhere
KCEventTransformer.m, KCMouseEventVisualizer.m, KCAppController.m, KCEventTap.mARC (per-file)COMPILER_FLAGS = -fobjc-arc set on the individual file
KCDefaultVisualizer.m, NSBezierPath+RoundedRect.m, MinimalVisualizer.mARC (target-level)targets have CLANG_ENABLE_OBJC_ARC = YES; KCDefaultVisualizer.m carries a #error if __has_feature(objc_arc) is false
SvelteVisualizer.m (same-dir target)—confirm from target before compiling

Consequence: the compiler invocation must pass ARC OFF by default (-fno-objc-arc -fobjc-weak) for the framework-class files but ON (-fobjc-arc -fobjc-weak) for the plugin sources — exactly the split implemented in the build below.

4. Investigation / Method

4.1 Merge upstream, keep custom files

  • Fetched upstream, then git merge 1e552a1 (fast-forward).
  • Quirk discovered: git merge FETCH_HEAD reported Already up to date even though the fork was 40 commits behind — because FETCH_HEAD from an earlier fetch was stale. Merging the explicit SHA 1e552a1 worked (Updating 58133a7..1e552a1, fast-forward).
  • Because upstream never touched KCDefaultVisualizer.{h,m}, the custom rewrite passes through untouched as M working-tree changes. git status --short = M keycastr/KCDefaultVisualizer.h, M keycastr/KCDefaultVisualizer.m.

4.2 Extract the ARC/MRC contract from the pbxproj (no Xcode needed)

Parsed KeyCastr.xcodeproj/project.pbxproj for three separate knobs: target-level CLANG_ENABLE_OBJC_ARC, per-file COMPILER_FLAGS = -fobjc-arc, and the implicit MRC default of the framework target. Result table in §3.1.

4.3 Syntax-phase validation of the whole merged tree

clang -fsyntax-only on all 17 .m sources with SDK-relative AppKit prefix (-include <SDK>/…/AppKit.h), -I keycastr -I <ShortcutRecorder>/Sources/ShortcutRecorder/include -I <Sparkle>/Sparkle, and per-file ARC/MRC flags → 0 errors on all 17 files. This replaced "does xcodebuild say it compiles".

4.4 Rebuild only the one plugin the custom code touches

Since KCDefaultVisualizer.m is the only source that diverges from stock, only that plugin is rebuilt. Everything else (including the SDK/bin app) is consumed prebuilt.

4.5 Surgical in-place app surgery + ad-hoc signing

Replacing a code-signed plugin inside a hardened-runtime app requires care (§5.4/§6.4): clean the bundle root, swap the plugin, ad-hoc sign the plugin, then ad-hoc sign the app without the runtime flag.

4.6 Headless runtime proof

Launch Contents/MacOS/KeyCastr directly, sleep, assert the process is alive, and lsof it to show the plugin bundle's main executable mapped into the process — proving NSBundle dlopen'd it.

5. Implementation — key pieces

PiecePathRole
Custom visualizer (working tree)keycastr/KCDefaultVisualizer.m / .hkeycap Core Animation renderer, ARC, #error ARC guard
ARC helperkeycastr/NSBezierPath+RoundedRect.mARC category used by the visualizer
Fixed upstream protocolkeycastr/KCVisualizer.hgained @optional noteKeyUpEvent: — optional, custom class unaffected
New upstream API present after mergekeycastr/KCEventTransformer.{h,m} (specialKeys, keyCapForKeystroke:), KCEventTap.{h,m}, KCAppController.m, KCMouseEventVisualizer.mthe 40-commit payload
Prebuilt basekeycastr/bin/KeyCastr.app.zipframeworks + nibs + stock plugins + signed app to patch
Built plugin (universal)…/KCDefaultVisualizer.kcplugin/Contents/MacOS/KCDefaultVisualizerour rebuilt bundle
Assembled appbuild/KeyCastr.app (gitignored build/)final deliverable

5.1 The no-Xcode compiler invocation (verified verbatim)

SDK=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
          WORK=/Users/void/Projects/keycastr
          AppKitH="$SDK/System/Library/Frameworks/AppKit.framework/Headers/AppKit.h"
          SR="$WORK/keycastr/Externals/ShortcutRecorder/Sources/ShortcutRecorder/include"
          SP="$WORK/keycastr/Externals/Sparkle/Sparkle"
          # ARC source (KCDefaultVisualizer.m + NSBezierPath+RoundedRect.m), done per arch:
          clang -c KCDefaultVisualizer.m -o <arch>/KCDefaultVisualizer.o \
            -arch <arch> -fobjc-arc -fobjc-weak -fobjc-runtime=macosx-11.0 -mmacosx-version-min=11.0 \
            -isysroot "$SDK" -I "$WORK/keycastr" -I "$SR" -I "$SP" \
            -include "$AppKitH" -Wno-deprecated-declarations

5.2 The link producing the @executable_path/../Frameworks load path

# For arm64 and x86_64 separately (ld is single-arch):
          clang -arch <arch> -bundle -mmacosx-version-min=11.0 \
            <arch>/KCDefaultVisualizer.o <arch>/NSBezierPath+RoundedRect.o \
            -framework Foundation -framework AppKit -framework QuartzCore \
            -F "$APP/Contents/Frameworks" -framework KCVisualizer \
            -o <arch>/KCDefaultVisualizer
          lipo -create arm64/KCDefaultVisualizer x86_64/KCDefaultVisualizer -output KCDefaultVisualizer

The -F "$APP/Contents/Frameworks" -framework KCVisualizer is the clever bit: the framework is taken straight from the unzipped shipped app, and the recorded load command is @executable_path/../Frameworks/KCVisualizer.framework — correct for any plugin sitting in Contents/PlugIns/… because at runtime @executable_path is the host Contents/MacOS directory.

5.3 Plugin bundle assembly (mirrors the stock layout)

KCDefaultVisualizer.kcplugin/
          ├── Contents/
          │   ├── Info.plist           # CFBundleIdentifier=io.github.keycastr.KCDefaultVisualizer,
          │   │                        # NSPrincipalClass=KCDefaultVisualizerFactory, NSMainNibFile=KCDefaultVisualizer
          │   ├── MacOS/KCDefaultVisualizer      # our universal Mach-O bundle (chmod +x)
          │   └── Resources/KCDefaultVisualizer.nib   # 24086 bytes, from the stock plugin/nib source tree

5.4 Helper: keycastr/KCDefaultVisualizer.nib/keyedobjects.nib

Kept from the shipped plugin; the nib is byte-identical to what the plugin already shipped with upstream, so no ui work was needed.

6. Experiments & Results — with Evidence

6.1 The merge — evidence

$ git log --oneline -3
          1e552a1 v0.11.1
          65f2a36 Fix Minimal visualizer settings layout warnings and add migration
          535a7ad Refactor preferences, to be a simple show/hide list that determines the label
          $ git status --short
           M keycastr/KCDefaultVisualizer.h
           M keycastr/KCDefaultVisualizer.m
          $ git log --oneline 58133a7..HEAD | wc -l
          40

✓ Fast-forward, custom files untouched, 40 commits picked up.

6.2 Syntax phase — evidence

All 17 .m sources via clang -fsyntax-only (flags per §3.1) → 0 errors. Files checked included the MRC framework core (KCVisualizer.m, KCKeystroke.m, KCMouseEvent.m, KCKeycastrEvent.m, …), per-file-ARC app sources (KCEventTap.m, KCAppController.m, …), and ARC plugin sources (KCDefaultVisualizer.m, NSBezierPath+RoundedRect.m, MinimalVisualizer.m).

6.3 Compile + link of the custom plugin — evidence

$ clang -c ... (arm64 + x86_64)              → exit 0, no diagnostics
            arm64/KCDefaultVisualizer.o   70208 B
            x86_64/KCDefaultVisualizer.o  69800 B
          $ clang ... -bundle ... -F <app>/Contents/Frameworks -framework KCVisualizer  → exit 0
          $ lipo -info KCDefaultVisualizer
            Architectures in the fat file: x86_64 arm64
          $ nm -m KCDefaultVisualizer | grep "_OBJC_CLASS"
            external _OBJC_CLASS_$_KCDefaultVisualizer
            external _OBJC_CLASS_$_KCDefaultVisualizerFactory
            external _OBJC_CLASS_$_KCDefaultVisualizerPreferencesView
            external _OBJC_CLASS_$_KCDefaultVisualizerWindow
          $ otool -L KCDefaultVisualizer | grep KCVisualizer
            @executable_path/../Frameworks/KCVisualizer.framework/Versions/A/KCVisualizer (...)

✓ Universal bundle, all four ObjC classes exported, runtime framework path correct.

6.4 Signing journey — three distinct failures, each instructive

  1. codesign --deep --force fails. Re-sealing the whole hierarchy breaks on the Sparkle XPC subcomponent ("sealed resource is missing or invalid … Sparkle.framework/Versions/Current/XPCServices/Installer.xpc") and hits "unsealed contents present in the bundle root". Solution: never --deep; sign the plugin and the app individually.
  2. AppleDouble zombies. The zip was created on macOS, so it contains ._* companion entries. unzip materializes them as real files inside the bundle:
KeyCastr.app/Contents/._PlugIns, ._Info.plist, ._MacOS, ._Resources, ._Frameworks …
          KeyCastr.app/Contents/MacOS/._KeyCastr

Any bare codesign --sign - then dies with unsealed contents present in the bundle root (exit 1), even when signing the bare inner executable. Solution: find "$APP" -name '._*' -delete (+ .DS_Store), then re-sign succeeds. Proof: the identical .o/binary signed fine as a standalone file (flags=0x2(adhoc)), and failed only when sitting inside the dirty bundle.

  1. Hardened runtime ≙ library validation ≙ ad-hoc plugin rejection. The stock app is signed TeamIdentifier=68TUCLNAPS … flags=0x10000(runtime). A hardened-runtime process enforces library validation: every loaded bundle must be Apple-signed or same-Team-ID. Our plugin would be ad-hoc (TeamIdentifier=not set) → rejected at dlopen. Solution: re-sign the app ad-hoc without the runtime option → the process no longer enforces library validation and can load the custom plugin. Note this drops hardened-runtime protection on the whole app — acceptable for a locally-built experimental build.

Final signature state:

Plugin:  CodeDirectory v=20400 flags=0x2(adhoc)   Signature=adhoc  TeamIdentifier=not set
          App:     CodeDirectory v=20400 flags=0x2(adhoc)   Signature=adhoc  TeamIdentifier=not set
          codesign --verify "$APP"   → exit 0  (clean)

6.5 Runtime proof — headless launch

$ "$APP/Contents/MacOS/KeyCastr" > launch.log 2>&1 &
          $ sleep 4; kill -0 $PID                          → process alive (APP IS RUNNING, no crash)
          $ lsof -p $PID | grep KCDefaultVisualizer
            KeyCastr <pid> txt REG .../KeyCastr.app/Contents/PlugIns/KCDefaultVisualizer.kcplugin/Contents/MacOS/KCDefaultVisualizer
          $ grep -iE "error|warn|crash|abort|terminate" launch.log   → (nothing)

✓ The app enumerated and dlopen'ed the custom plugin (the plugin binary is mapped into the process), and the log is empty. This is as close to "it works" as a headless box with no Input Monitoring permission can get.

6.6 Deliverable

Repo build/KeyCastr.app (gitignored) — full app with the custom plugin inside, ad-hoc signed, codesign --verify clean, universal plugin binary. A staged copy of the plugin also lives at build/KCDefaultVisualizer.kcplugin.

7. Conclusions

  1. The merge is safe: v0.11.1 fast-forwards cleanly over the fork because upstream never touched KCDefaultVisualizer.{h,m}; the custom rewrite survives verbatim as uncommitted working-tree changes.
  2. git merge FETCH_HEAD is untrustworthy after a prior fetch — merge the pinned SHA (git merge 1e552a1), not the ref.
  3. The build story is proven without Xcode: clang -fsyntax-only for the whole tree (0 errors) + clang -bundle of the one custom plugin + reuse of the shipped .app zip as dependency/skeleton is a valid substitute for xcodebuild.
  4. ARC/MRC is a per-file contract that must be read from project.pbxproj in three places (target ARC flag, per-file -fobjc-arc, MRC default). Means: framework MRC, app files partially per-file ARC, visualizer plugin ARC.
  5. macOS-made zips contain ._* AppleDouble files that unzip puts inside the bundle; they silently break re-signing with "unsealed contents" — delete before signing.
  6. Never blanket --deep re-sign complex signed apps (Sparkle XPC breaks); patch the changed leaf and re-sign the app in place.
  7. Ad-hoc plugin + hardened-runtime host are incompatible (library validation) unless the app is re-signed without the runtime flag — a legitimate, if protection-lowering, tradeoff for a local build.
  8. Process-level verification is enough when GUI/Accessibility isn't available: lsof of plugin mapping + clean log + alive process ∥ "the plugin loads".

8. Final Configuration / Recommendations

ConcernAdopted
Upstream syncmain fast-forwarded to 1e552a1 (v0.11.1); 40 commits; not pushed
Custom codeKCDefaultVisualizer.{h,m} kept as uncommitted working-tree changes (intentional)
CompilerCLT clang, SDK …/CommandLineTools/SDKs/MacOSX.sdk, -mmacosx-version-min=11.0
Plugin build flags-fobjc-arc -fobjc-weak, -include <SDK> …/AppKit.h prefix, -I keycastr -I <ShortcutRecorder>/…/include -I <Sparkle>/Sparkle
Linkclang -bundle -arch arm64·x86_64 + lipo -create; -F <app>/Contents/Frameworks -framework KCVisualizer
Plugin identityInfo.plist: bundle id io.github.keycastr.KCDefaultVisualizer, NSPrincipalClass=KCDefaultVisualizerFactory, NSMainNibFile=KCDefaultVisualizer
Signingfind <app> -name '._*' -delete; codesign --force --sign - <plugin> then codesign --force --sign - <app> (no --deep, no --options runtime)
Runtime proofheadless launch + `lsof -p <pid>grep KCDefaultVisualizer`
Deliverablebuild/KeyCastr.app + build/KCDefaultVisualizer.kcplugin (both gitignored)

Key files: keycastr/KCDefaultVisualizer.{h,m} (custom, uncommitted), keycastr/KeyCastr.xcodeproj/project.pbxproj (ARC/MRC + version 0.11.1), keycastr/KCVisualizer.h, keycastr/KCEventTransformer.{h,m}, keycastr/bin/KeyCastr.app.zip, build/KeyCastr.app.

9. Appendix — Reproduction

# ---- 0) Prereqs ----
          # macOS CLT only (no Xcode); SDK present at:
          ls /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
          WORK=/Users/void/Projects/keycastr
          TMP=/var/folders/y9/gsb6gx0x7bn1d1r2370sknc40000gp/T/xgcode/keycastr-build
          SDK=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
          SR="$WORK/keycastr/Externals/ShortcutRecorder/Sources/ShortcutRecorder/include"
          SP="$WORK/keycastr/Externals/Sparkle/Sparkle"
          APPH="$SDK/System/Library/Frameworks/AppKit.framework/Headers/AppKit.h"

          # ---- 1) Merge upstream (submodules already init'd) ----
          cd "$WORK"
          git fetch https://github.com/keycastr/keycastr
          git merge 1e552a1            # explicit SHA — do NOT trust `git merge FETCH_HEAD`
          git status --short           # expect: only the two custom files still " M"

          # ---- 2) Whole-tree syntax check (0 errors expected) ----
          for f in $(git ls-files '*.m'); do
            case "$f" in
              *KCDefaultVisualizer.m|*NSBezierPath+RoundedRect.m) ARC="-fobjc-arc";;
              *KCEventTransformer.m|*KCMouseEventVisualizer.m|*KCAppController.m|*KCEventTap.m) ARC="-fobjc-arc";;
              *) ARC="-fno-objc-arc -fobjc-weak";;
            esac
            clang -fsyntax-only $ARC -fobjc-weak -include "$APPH" \
              -I "$WORK/keycastr" -I "$SR" -I "$SP" "$WORK/$f" || echo "FAIL $f"
          done

          # ---- 3) Build the custom plugin (universal) ----
          rm -rf "$TMP/out"; mkdir -p "$TMP/out"
          for A in arm64 x86_64; do
            mkdir -p "$TMP/out/$A"
            clang -c "$WORK/keycastr/KCDefaultVisualizer.m" -o "$TMP/out/$A/KC.o" \
              -arch $A -fobjc-arc -fobjc-weak -fobjc-runtime=macosx-11.0 -mmacosx-version-min=11.0 \
              -isysroot "$SDK" -I "$WORK/keycastr" -I "$SR" -I "$SP" -include "$APPH" -Wno-deprecated-declarations
            clang -c "$WORK/keycastr/NSBezierPath+RoundedRect.m" -o "$TMP/out/$A/BP.o" \
              -arch $A -fobjc-arc -fobjc-weak -fobjc-runtime=macosx-11.0 -mmacosx-version-min=11.0 \
              -isysroot "$SDK" -I "$WORK/keycastr" -I "$SR" -I "$SP" -include "$APPH" -Wno-deprecated-declarations
          done

          # Extract the shipped app to borrow frameworks + skeleton:
          rm -rf "$TMP/KeyCastr.app"; unzip -q "$WORK/keycastr/bin/KeyCastr.app.zip" -d "$TMP/"
          F="$TMP/KeyCastr.app/Contents/Frameworks"

          for A in arm64 x86_64; do
            clang -arch $A -bundle -mmacosx-version-min=11.0 \
              "$TMP/out/$A/KC.o" "$TMP/out/$A/BP.o" \
              -framework Foundation -framework AppKit -framework QuartzCore \
              -F "$F" -framework KCVisualizer -o "$TMP/out/$A/KCDefaultVisualizer"
          done
          lipo -create "$TMP/out/arm64/KCDefaultVisualizer" "$TMP/out/x86_64/KCDefaultVisualizer" \
            -output "$TMP/out/KCDefaultVisualizer"
          nm -m "$TMP/out/KCDefaultVisualizer" | grep "_OBJC_CLASS"   # 4 classes expected

          # ---- 4) Assemble the .kcplugin bundle ----
          P="$TMP/KCDefaultVisualizer.kcplugin"
          mkdir -p "$P/Contents/MacOS" "$P/Contents/Resources"
          cp "$TMP/KeyCastr.app/Contents/PlugIns/KCDefaultVisualizer.kcplugin/Contents/Info.plist" "$P/Contents/"
          cp "$TMP/KeyCastr.app/Contents/PlugIns/KCDefaultVisualizer.kcplugin/Contents/Resources/KCDefaultVisualizer.nib" "$P/Contents/Resources/"
          cp "$TMP/out/KCDefaultVisualizer" "$P/Contents/MacOS/"; chmod +x "$P/Contents/MacOS/KCDefaultVisualizer"

          # ---- 5) Swap + clean + re-sign (no --deep; no hardened runtime) ----
          rm -rf "$TMP/KeyCastr.app/Contents/PlugIns/KCDefaultVisualizer.kcplugin"
          cp -R "$P" "$TMP/KeyCastr.app/Contents/PlugIns/KCDefaultVisualizer.kcplugin"
          find "$TMP/KeyCastr.app" -name '._*' -delete        # CRITICAL or signing fails
          find "$TMP/KeyCastr.app" -name '.DS_Store' -delete
          codesign --force --sign - "$TMP/KeyCastr.app/Contents/PlugIns/KCDefaultVisualizer.kcplugin"
          codesign --force --sign - "$TMP/KeyCastr.app"
          codesign --verify "$TMP/KeyCastr.app" && echo SIGNED-AND-VALID

          # ---- 6) Runtime proof (headless) ----
          "$TMP/KeyCastr.app/Contents/MacOS/KeyCastr" > "$TMP/launch.log" 2>&1 &
          PID=$!; sleep 4
          kill -0 $PID && echo ALIVE
          lsof -p $PID | grep -c KCDefaultVisualizer       # ≥1 → plugin dlopen'ed
          kill $PID

          # ---- 7) Deliverable ----
          mkdir -p "$WORK/build"
          rm -rf "$WORK/build/KeyCastr.app"
          cp -R "$TMP/KeyCastr.app" "$WORK/build/KeyCastr.app"

10. Quick takeaways — the "worth note-taking" list

Technical (with evidence):

  1. git merge FETCH_HEAD lies — it said "Already up to date" against a 40-commit-behind branch. Always merge the explicit SHA.
  2. No-Xcode build is viable via three legs: CLT clang -fsyntax-only for whole-tree proof, clang -bundle rebuild of only the changed plugin, and reuse of the shipped .app zip for frameworks/nibs/skeleton. 0 syntax errors on all 17 sources; universal plugin linked with 4 exported classes.
  3. -F $APP/Contents/Frameworks -framework KCVisualizer while linking the plugin yields exactly the right runtime load path @executable_path/../Frameworks/… — no install_name_tool needed.
  4. ARC/MRC is a per-file contract read from project.pbxproj in three ways: framework target MRC by default, four app files get per-file -fobjc-arc, and the visualizer plugins are ARC at target level (with a #error guard in KCDefaultVisualizer.m).
  5. AppleDouble zombies: macOS-made zips carry ._* entries that unzip turns into real files inside the bundle root → any re-sign fails with unsealed contents present in the bundle root (exit 1). find -name '._*' -delete fixes it. Proven by regression: same binary signed fine standalone, failed only inside the dirty bundle.
  6. codesign --deep is wrong for this: it re-seals Sparkle's Installer.xpc and breaks. Patch the leaf plugin, then sign plugin + app separately.
  7. Ad-hoc plugin vs hardened runtime: flags=0x10000(runtime) implies library validation → Team-ID-less plugin rejected on dlopen. Ad-hoc re-signing the app (no --options runtime) removes the barrier. Trade-off: the app loses hardened-runtime protection.
  8. Headless runtime proof = lsof + empty log + alive process; that's exactly as far as a box without Input Monitoring permission can verify keystroke visualizers.

Operational:

  1. Fork hygiene: don't commit the custom visualizer into the same diff as the upstream fast-forward; keep it as the intentional working-tree delta so upstream's next merge stays clean.
  2. Deliverable placement: gitignored build/ dir keeps the patched app out of git while remaining repo-relative and easy to find.

Open follow-ups (not done in this session): pushing main (40 commits ahead of origin), committing the custom visualizer changes, granting Accessibility + Input Monitoring to actually see the keycaps render, and optionally running KCVisualizerTests.xctest.

XGcode: Forking OpenCode, Rebrand, and the XG Gateway Integration

To own a distinct CLI while staying compatible with the opencode ecosystem, OpenCode was forked into XGcode (private repo ahzslanh/xgcode, branch dev) and the XperGentium worker/orchestrator was rewired to run it. The work had three phases: (1) a full product rebrand (OpenCode→XGcode, scope @opencode-ai→@xgcode, config .opencode/opencode.json→.xgcode/xgcode.json, CLI command opencode→xgcode), (2) an XG Gateway integration so xgcode talks to the XG swarm (memory/RAG/orchestration) instead of owning that logic, and (3) a set of stability fixes discovered by building and running live tasks. Headline lessons: a rebrand is content-safe but filename/contract-hazardous — sed rewrites strings, not on-disk filenames, and prebuilt vendored artifacts + protocol/provider identifiers cannot be renamed without breaking builds. Product name is XGcode; internal/contractual identities stay OpenCode/opencode (vendored client exports, provider id, protocol fields, Zen infra). For local-LAN LLM routing, the fork's native ollama provider resolves via the unreachable models.dev catalog, so all model resolution must go through an OpenAI-compatible provider block (baseURL = <gateway>/v1). Two real bugs fixed: D:\XGcode worker crash (reference.ts realPath/orDie on a nonexistent local dir) and ProviderModelNotFoundError on native ollama/* refs.

xgcode opencode fork rebrand bun monorepo xpergentium gateway mcp llm ollama provider-routing models.dev swarm

2. Background & Problem Statement

  1. Distinguish own tool from upstream. The user runs opencode daily inside nvim and wants a fork (XGcode) it can recognize as its own, while treating upstream opencode as a mere "maintenance feed" — cherry-pick security/bug/LSP fixes only, never wholesale-merge.
  2. Free models are not forever. opencode's free models (hy3-free, nemotron-*, big-pickle, muse-spark-1.2-contributor-free, mimo-v2.5-free) are documented as "available for a limited time to collect feedback". The durable path is local models + a persistent memory layer.
  3. Memory must be the spine, not the models. In a multi-agent swarm, models are commodities; decisions/conventions/contracts live in memory + vault (Postgres-backed), which survives any model swap.
  4. XGcode must not own brain logic. Architecture decided: nvim → xgcode (execution interface) → XG Gateway (intelligence: routing/memory/RAG/orchestration) → Postgres (knowledge) / local LLM (reasoning).

3. Environment / Setup

ComponentDetail
Mac (this host)User void; /Volumes/Xbyte/ root-owned; workspace /Volumes/Xbyte/Projects/XperGentium
XGcode repohttps://github.com/ahzslanh/xgcode (private), branch dev; origin = fork, upstream = anomalyco/opencode
Windows PC192.168.0.192, Ollama host "remote-windows"; Coder agent = qwen3-coder:30b
OpenCode sourceBun/TypeScript monorepo (packages/), root has NO build script; build via packages/opencode/script/build.ts
ToolchainBun 1.4.0 at /opt/homebrew (bun install → 2315 packages)
Vendored clientpackages/app/vendor/opencode-ai-client-1.17.13-v2.tgz (resolves as @xgcode/client) — prebuilt, exports OpenCode/OpenCodeClient/OpenCodeEvent (capital C)
XG DBpostgresql:///xpergentium; projects/tasks/memory/events; memory types: semantic, episodic, procedural, project, technical
Local LLMqwen3-coder:30b on PC; gateway proxy 127.0.0.1:11435 → LAN Ollama
Logs~/Library/Logs/xg-gateway.log, ~/Library/Logs/xg-queue.log, ~/.local/share/xgcode/log/opencode.log

4. Investigation / Method — The Rebrand

4.1 Initial wrong assumption

The session assumed opencode was a Go monorepo; the first build attempt found it is actually Bun/TypeScript (clone + package.json inspection). Assumption corrected early, saved a wrong methodology.

4.2 Rebrand strategy (user-formed)

  • Product identity (rename): display XGcode, npm scope @xgcode/*, config dir .xgcode, config file xgcode.json, CLI command xgcode.
  • Internal compatibility (keep): opencodeComment (protocol field), OpenCode protocol/provider identifiers, built-in provider id opencode, Zen infrastructure, MCP originator headers, telemetry user-agents, workspace package name opencode.
  • Divergence model: git fetch upstream && git cherry-pick <sha> for security/bug/LSP fixes only.

4.3 Sed-only renames are not filesystem renames

Mechanical replaces succeeded in content, but the build broke because import strings became ./themes/xgcode.json while the files on disk were still opencode.json. Fix: git mv the two theme files (packages/ui + packages/tui). Lesson above all others for this session: after a string-replace rebrand, enumerate and rename matching on-disk files; verify batched sed actually operated (first run produced an empty file list — the grep enumeration failed silently). BSD sed needs sed -i ''; use git grep not rg (not installed); --include globs break under zsh.

4.4 Vendored artifact export conflicts

@xgcode/client is two packages: the workspace packages/client (SDK source, exports /effect) and the vendored tgz (exports /promise). The rebrand renamed workspace exports to XGcode*, but the prebuilt tgz still exported OpenCode* — causing "XGcode" is not exported by "@xgcode/client/promise". Resolution: packages/client imports XGcode* from @xgcode/protocol and bridges/re-exports publicly as OpenCode*; all consumers (sdk-next, tests, server-sdk.tsx) import OpenCode*. Vendored/prebuilt artifacts are rename-impassable.

4.5 Incomplete-rename cascade

The camelCase opencode→xgcode rename applied inconsistently — definitions kept opencode* while usages became xgcode* (e.g. opencodeArgs/xgcodeArgs, opencodeComment, opencodeChecks, ProviderV2.ID.opencode). Each half-rename broke typecheck in a new location (desktop, app, schema, acp tests). The user rule resolved the policy: product identity renamed, contractual identifiers reverted to opencode, then typecheck was iterated to green.

5. Experiments & Results — with Evidence

5.1 Build verification on Mac (swarm runner was broken)

Swarm dispatch attempts for the build all failed, proving the task runner was broken at that time:

TaskAgentResult
#447coderrejected: "subagent, not a primary agent"
#450maintainerrejected: "agent not found" (not in dispatch registry)
#452planneraccepted, then opencode server crashed on spawn: err_a2cd55ee, died ~6s, no build

Fallback: local Mac build. bun install (2315 pkgs, 45s) → bun run --cwd packages/opencode build → after the theme-file git mv and vendored-client revert, build passed (smoke test 0.0.0-dev-202608271707), then full bun typecheck green (30/30 turbo tasks). Verified CLI: xgcode --version → 0.0.0-dev-202608271821, xgcode --help shows xgcode completion, xgcode acp, etc.

Commit trail: 5dd94a6f5 (full rebrand), d7fcdc1ea (deeper @xgcode/ service tags + Xgcode camelCase + swarm model-map), cd03fa025 (build fixes), 4008d2ea7 (typecheck green), 64c8c6768 (packaging fixes), abbc50bf7 (reference.ts crash fix in fork).

5.2 Functionally-broken packaging found by audit (fixed per strict rule)

  • publish.ts npm bin field still opencode → ./bin/opencode.exe while postinstall.mjs installs bin/xgcode → published command would be dead → fixed to xgcode → ./bin/xgcode.exe.
  • nix/opencode.nix + Dockerfile copied dist/opencode-*/bin/opencode (file gone) → bin/xgcode.
  • packages/core/package.json dangling bin → xgcode.
  • settings-model.test.ts expected /usr/local/bin/opencode → xgcode.
  • Docs web/acp.mdx ×20 languages + CONTRIBUTING.md showed bin/opencode.
  • Rule applied strictly: fix only what is functionally wrong for build/packaging/runtime/user-facing; leave upstream CI, .github/, brand-asset filenames, opencode.ai domain, attribution.

5.3 XG rewire (XperGentium repo, commit a802286)

  • .opencode/opencode.json → .xgcode/xgcode.json
  • opencode_worker.py → xgcode_worker.py; entrypoint xg-opencode-worker → xg-xgcode-worker; orchestrator worker_cmd updated; spawn now xgcode run.
  • scripts/model_failover.py: config/sidecar paths → .xgcode/*, execvpe("opencode") → execvpe("xgcode").
  • Correctly kept: provider IDs (opencode/), opencode.ai hosts, OPENCODE_CONFIG env override, session-DB path ~/.local/share/opencode/opencode.db (pkg name stays opencode internally).

5.4 XG Gateway (execution vs intelligence split)

Chain after integration:

nvim → xgcode (execution interface)
                  └ xg-xgcode-worker → `xgcode run --agent <slug>` (merged config via OPENCODE_CONFIG)
                       └ MCP xg-mcp        (orchestration: tasks/memory)
                       └ MCP xg-mcp-exec   (memory/RAG/context) → Postgres
                       └ provider: openai-compatible local LLM (gateway) or cloud

gateway.py = new xg-gateway proxy 127.0.0.1:11435 → LAN Ollama; seed/migration captured in db/seed.sql, schema.sql, migrate_memory_rag.sql; XGDev.md corrected (v0.9.6 → v0.9.7).

5.5 Bug #1 — D:\XGcode worker crash (root cause found)

Symptom: worker tasks failed with Unexpected server error / err_a2cd55ee; odd D:\XGcode path appearing on the Mac. Root cause: opencode materialized a local reference whose directory was the Windows build path D:\XGcode, then FileSystem service init (filesystem.ts:65) realPath'd it with orDie → ENOENT crash on Mac. Fix in fork packages/core/src/reference.ts: skip local references whose directory doesn't exist; rebuilt binary (bun run script/build.ts --single --skip-embed-web-ui).

5.6 Bug #2 — gateway routing silently wrong (exposed by verification)

During live-task testing the worker used native ollama/qwen3-coder:30b + OLLAMA_HOST=127.0.0.1:11435 and failed with ProviderModelNotFoundError: Model not found: ollama/qwen3-coder:30b. Root cause: the fork's built-in ollama provider has no OLLAMA_HOST env handling — OLLAMA_HOST is unreferenced in the fork's source — and resolves models via the models.dev catalog (unreachable offline). Restored the proven mechanism: declare an OpenAI-compatible openai provider with baseURL = <gateway>/v1 and resolve everything to openai/<tag> (small_model + all agent models). End-to-end proof: task 12 completed, created Vault/Projects/Python-test/gw_bootstrap11.txt = bootstrap-gateway-ok-11; gateway log POST /api/generate model='qwen3-coder:30b' → 200 OK (desired model, no fallback).

5.7 Orchestrator failure-classification hardening (already committed in orchestrator.py)

_MODEL_UNAVAILABLE_SIGNALS tightened; _ollama_model_present existence check via /api/tags; cloud models without API key excluded from pick_fallback_model; _route_failed availability-gated via _model_is_available.

5.8 Installer / UNinstaller / README

install.sh (detect existing clone → else clone ahzslanh/xgcode, build or reuse binary, link xgcode into ~/.local/bin, no sudo; XG_PREFIX overridable; Bun bootstrapped if missing), uninstall.sh, README Install/Uninstall section at repo root. One-liner: curl -fsSL https://raw.githubusercontent.com/ahzslanh/xgcode/dev/install.sh | bash. Note: command is lowercase xgcode (shells are case-sensitive — nvim :terminal didn't recognize XGcode).

6. Conclusions

  1. Product vs contract identity is the organizing rule of a safe fork. Rename user-facing/display/config/scope; keep internal/contractual identifiers (opencode provider id, protocol fields, vendored exports, package name, DB path, OPENCODE_CONFIG). Deviations cost several typecheck/build iterations.
  2. sed renames content, never filenames. After a bulk replace, enumerate and git mv matching on-disk files; verify the replace actually ran (batched loops fail silently).
  3. Prebuilt/vendored artifacts block renames. The vendored @xgcode/client tgz pinned OpenCode* exports; the workspace SDK must bridge (XGcode* internal → OpenCode* public).
  4. The command name change ripples into packaging. publish.ts, postinstall.mjs, build.ts, Nix, Dockerfile, tests, and i18n/docs all reference the binary name; each must be audited for dangling/incorrect paths.
  5. Native ollama provider is unreliable for this fork. It resolves via models.dev (unreachable) and ignores OLLAMA_HOST; route all local models through an OpenAI-compatible provider block pointing at the gateway.
  6. XG Gateway works end-to-end. xgcode → worker → gateway(nginx-style proxy) → local LLM verified live with task 12; the gateway owns routing/memory/RAG; XGcode stays a thin execution interface.
  7. Swarm model policy: models rotate freely; xmpergentium_memory_set/vault/AGENTS.md are the durable spine. Keep the agent→model→host map committed (swarm-model-map.json + SWARM.md).

7. Final Configuration / Recommendations

Identity scopeValue
Product/displayXGcode, scope @xgcode, config dir .xgcode, config file xgcode.json, CLI command xgcode
Internal/contract (keep)provider id opencode, opencodeComment, Zen infra, vendored OpenCode* exports, opencode-ai npm pkg name, ~/.local/share/opencode/opencode.db
Divergencegit fetch upstream && git cherry-pick <sha> — security/bug/LSP only
Local LLM routingOpenAI-compatible openai provider, baseURL = <gateway>/v1, models openai/*
Gatewayxpergentium/gateway.py proxy 127.0.0.1:11435 → LAN Ollama (qwen3-coder:30b)
Install`curl -fsSL https://raw.githubusercontent.com/ahzslanh/xgcode/dev/install.sh \bash (→ ~/.local/bin/xgcode`)

Key files:

  • Fork: packages/opencode/src/config/config.ts (config-file lookup), packages/opencode/script/publish.ts + postinstall.mjs + build.ts, packages/core/src/reference.ts (crash fix), packages/client/src/effect.ts (bridge), swarm-model-map.json, SWARM.md, install.sh, uninstall.sh
  • XG: xpergentium/xgcode_worker.py, xpergentium/gateway.py, xpergentium/orchestrator.py, scripts/model_failover.py, .xgcode/xgcode.json, db/seed.sql/schema.sql/migrate_memory_rag.sql

8. Appendix — Reproduction Commands

# Build XGcode from source
          bun --version                                  # needs Bun ≥1.4
          bun install
          bun run --cwd packages/opencode build         # binary → packages/opencode/bin/xgcode
          bun run typecheck                             # pre-push gate (30/30 turbo tasks)

          # Install / uninstall on any machine
          curl -fsSL https://raw.githubusercontent.com/ahzslanh/xgcode/dev/install.sh | bash
          bash /path/to/xgcode/install.sh
          bash /path/to/xgcode/uninstall.sh

          # Verify fork behaves
          xgcode --version
          xgcode --help                                 # shows xgcode completion, xgcode acp ...

          # Port an upstream fix
          git fetch upstream && git cherry-pick <sha>

          # Start XG gateway + queue (XperGentium root)
          ./start.sh                                    # exposes 127.0.0.1:11435 → LAN Ollama

          # Probe gateway model resolution
          curl -s http://127.0.0.1:11435/api/tags       # must list qwen3-coder:30b

          # Live end-to-end worker test (from prior session)
          # queue a task in a project with working dir; expect .xg/result-<N>.json + proof file

Cleanup note: XperGentium worker/orchestrator changes committed locally (a802286, 2527df4); pushed to origin/dev as required. Fork commits pushed (5dd94a6f5 … abbc50bf7). Session DB (~/.local/share/opencode/opencode.db) retains full history until policy-based prune.

High-CPU Investigation & Remediation

High CPU on a Windows PC was NOT CompatTelRunner. Root cause: CopyGuard's DiskSentinel module failing to read NTFS USN journal, re-querying every 5s. Secondary: WmiPrvSE at 140%+ CPU from broken LineageTracker WMI watcher.

copyguardcpudisk-sentinel usn-journalwmiwindows

root cause

  • DiskSentinel couldn't read USN journal (error 1784), retried every 5s
  • WmiPrvSE at 140%+ from orphaned WMI subscriptions
  • CompatTelRunner was NOT running

fix

  • Disabled broken modules via policy.json
  • Increased poll interval 5s → 30s
  • Recreated USN journal, cleaned orphaned WMI subscriptions

Local LLM Failover & Planner Benchmark

When cloud free models go down, XG must keep operating. Built failover to LAN ollama. Benchmarked planner models: qwen2.5:14b is competent, qwen3:8b fails as planner despite correct tool-calling in isolation.

qwen2.5:14bqwen3:8bfailover ollamabenchmarkplanner

findings

  • qwen2.5:14b — correct task decomposition, drives full swarm to completion
  • qwen3:8b — tool-calls correctly in isolation but fails as planner
  • gemma3:12b — ruled out, cannot emit tool calls via ollama
  • Secondary: qwen2.5:14b defaults to Chinese unless pinned to English

Zero-Budget IAM Command Centre

Full identity and access oversight, built in 3 months with zero budget and zero enterprise tools. No PAM, no CIEM — just Google Sheets, Notion, and Apps Script on cron jobs edited in nvim. Live user DB, internal-vs-external login monitoring, ghost-account tracker, RBAC visibility, calendar sync, a Slack bot that fed dev and HR in real time, and a Notion hub that onboarded new staff on day one.

iamzero-budgetapps-script google-sheetsnotionslackrbac

the stack

  • Google Apps Script (CLASP) on cron triggers, edited in nvim
  • Google Sheets as the live database; FastAPI/REST where spreadsheets weren't enough
  • Slack + Notion as the front door — tools people already lived in

what it did

  • Weekly HR export → live user DB — the baseline everything checked against
  • Login monitor distinguishing internal vs external access, tracking IP patterns
  • Ghost tracker cross-checked accounts across every system for abandoned/unknown identities
  • RBAC visibility per user across all tools
  • Calendar sync engine, Slack bot (bugs → dev, candidates → HR), Notion onboarding hub

the lesson

  • Zero budget forces the data model right — you can't buy the product that hides messy joins
  • Internal-vs-external visibility is most of entry-level IAM; ghost tracking is the rest
  • Pulse is what SF was built from
1