Context
A few days ago I shipped v0.2 and v0.3 of KIOKU, adding PDF and URL ingestion to my Claude Code / Desktop memory system. The features were working.
What I wanted to do next wasn't adding more. It was reading the code I'd already shipped as if someone else had written it.
v0.4.0 is the result. Zero new features. But every fix in it carries the same feeling: glad I caught this before someone else did.
This post is about three of those fixes — the ones that genuinely made me pause. It's not a feature post, it's the "what I found once I started looking" post.
https://github.com/megaphone-tokyo/kioku
Mac mini was silently failing git push for five days
This was the one that made me sweat.
KIOKU syncs your Obsidian vault across machines via Git. I run it on a MacBook and a Mac mini, both doing git commit && git push from auto-ingest.sh. Knowledge written on one machine shows up on the other. That's the whole point.
Except one afternoon I checked the Mac mini's log and realized: push hadn't succeeded in five days.
What was happening
auto-ingest.sh runs git commit followed by git push. The commits were landing. reflog confirmed — there was a wall of commits accumulated locally. The problem was on the push side:
$ cd $OBSIDIAN_VAULT
$ git status
HEAD detached at abc1234
Detached HEAD.
I don't know exactly when it happened. Probably some Obsidian-side operation — an aborted rebase, a half-finished branch switch. HEAD had fallen off its branch and stayed there.
In detached HEAD state, git commit succeeds. It just advances HEAD. But git push can't decide which remote branch to push to, so it fails. Fails silently, too — auto-ingest.sh was swallowing the exit code with || true.
Result: commits piling up in reflog, zero reaching origin, for five days.
The fix: guard on git symbolic-ref
The fix is tiny. In auto-ingest.sh / auto-lint.sh / install-hooks.sh, check HEAD before any git writes:
if ! git symbolic-ref -q HEAD >/dev/null 2>&1; then
echo "WARNING: Vault is in detached HEAD state. Skipping git commit/push." >&2
echo "Recovery: run 'git checkout main' in the Vault." >&2
return 0
fi
git symbolic-ref -q HEAD returns refs/heads/<branch> when attached, fails when detached. One conditional, done.
What it taught me
The nasty thing about this class of bug is that it's an error that doesn't show up as an error. Commit succeeds. Push fails, but fails quietly because of a well-meaning || true. The logs are clean. The process exits zero.
Meanwhile, the premise underneath the whole product is eroding. KIOKU's second-brain promise is "your knowledge follows you between machines." Five days of silent desync breaks that, even if nothing explicitly broke.
"Works" and "works correctly" are not the same thing. Failsafes like || true are useful, but what exactly are you silently swallowing? is a question I now want to be able to answer for every one of them in my code.
The MCP lock was being held for 4+ minutes
Not a correctness bug, but an architectural one that hurt.
KIOKU's MCP server uses a lockfile at $VAULT/.kioku-mcp.lock to serialize writes between auto-ingest.sh (cron-driven) and MCP tools (Claude Desktop / Code driven). Both touch the same vault, so they need to take turns.
withLock is the helper — acquire, run, release. Standard stuff.
The problem was in how kioku_ingest_url handled PDF URLs:
await withLock(vault, async () => {
// ...fetch URL, save PDF to raw-sources/...
if (isPdf) {
await handleIngestPdf(vault, { path: savedPath }, { skipLock: true });
// ^^^^^^^^^^^^^^
// "we already own the lock, don't re-acquire"
}
});
The outer withLock was holding the lock while the inner call did the full PDF pipeline — poppler extraction, chunking, summary via claude -p. On a 50-page PDF, measured hold time: 4.5 minutes.
For those 4.5 minutes, every cron ingest run, every other MCP tool call, was blocked waiting for the lock.
The fix: release the outer lock first
The refactor: scope withLock to just "write the PDF to disk" (seconds), then release before dispatching to the PDF handler:
const phase1Result = await withLock(vault, async () => {
// fetch URL, save PDF — finishes in seconds
return { savedPath, needsPdfDispatch: true };
});
// lock is released here
if (phase1Result.needsPdfDispatch) {
// handleIngestPdf acquires its own withLock when it needs to
await handleIngestPdf(vault, { path: phase1Result.savedPath });
}
Once I restructured it this way, the skipLock injection was no longer needed for anything. I deleted it from the API entirely.
What it taught me
The existence of skipLock was itself a design smell. "Flag to avoid double-acquiring the lock" only makes sense if the caller knows whether it already holds the lock — and that's coupling I shouldn't have leaked into the API surface.
The correct shape is: every callable that needs the lock takes it itself, locks are reentrant or short-lived enough not to matter, and no one has to reason about "what state is the caller in."
Pre-release, when I knew all the callers personally, skipLock was "good enough." As soon as KIOKU was public, anyone could call handleIngestPdf through a different path. skipLock immediately becomes a trap.
Shortcuts that rely on "I know all my callers" rot on contact with publication.
Hidden bypass holes in the Hook layer
The smallest-feeling but scariest of the three.
KIOKU's Hook captures Claude Code sessions and masks secrets before writing them to disk — sk-ant-... becomes sk-ant-***, same for OpenAI, GitHub, AWS, Slack, the usual cast. MASK_RULES is an array of regexes.
Session logs get committed to GitHub (private repo, but still — commit history is forever). If masking misses something, there's no taking it back.
Bypass hole 1: zero-width space slip-through
During the v0.4 audit of the Hook layer, I noticed this:
sk-ant-abcdefghijklmnopqrstuvwxyz
↑
U+200B (zero-width space)
Insert a zero-width space between sk-ant- and the body of the token, and the regex /sk-ant-[A-Za-z0-9_-]{20,}/ doesn't match. Visually it's indistinguishable from a normal token. The mask silently does nothing.
Realistic scenarios where this could occur:
- Someone deliberately injects invisible chars into a prompt
- A text editor paste drags invisible chars along
- A Markdown preprocessor inserts zero-width chars
The point is: regex matching alone is insufficient as a masking strategy when the input space includes Unicode.
Bypass hole 2: YAML frontmatter injection
Session logs have YAML frontmatter:
---
session_id: abc123
cwd: /Users/me/project
---
These values come from the hook execution environment — mostly trustworthy, but some like cwd can contain newlines.
If an attacker could engineer cwd to be:
/tmp/x\n---\ntype: injected\nrelated: ["/etc/passwd"]
…the frontmatter gets closed mid-value, and injected type: / related: keys appear as if they were set legitimately. Any downstream tool that trusts frontmatter would act on the forged values.
Bypass hole 3: KIOKU_NO_LOG strict-equality drift
KIOKU's auto-ingest spawns claude -p to process logs. That claude -p is itself a Claude Code session that fires Hooks. Without a guard, you get infinite recursion: ingest → spawn → hook fires → ingest → spawn → ...
The guard is a KIOKU_NO_LOG=1 env var:
if (process.env.KIOKU_NO_LOG === '1') return;
Problem: strict equality with '1'. If anyone — me, a future contributor, a user automating KIOKU — writes KIOKU_NO_LOG=true or KIOKU_NO_LOG=yes thinking the obvious thing, the guard silently stops working. The recursion comes back.
Fix: proper defense in depth
All three now fixed:
-
Masking: strip
INVISIBLE_CHARS_REand NFC-normalize before regex matching -
Frontmatter:
yamlSafeValue()scrubs control characters and YAML structure chars before quoting -
Env check:
envTruthy()accepts1 / true / yes / on(case-insensitive)
What it taught me
None of these bugs had caused harm, as far as I could tell. No zero-width-space tokens were observed. No YAML injection attempted. Nobody had miswritten KIOKU_NO_LOG=true. Every one of them was "this could happen" territory.
But the asymmetry for security bugs is brutal: "could happen" is cheap to fix, "did happen" is often impossible to undo. A leaked API key is in GitHub history forever.
If KIOKU had stayed private, I'd probably have shrugged these off. I don't smuggle zero-width chars into my own prompts. Nobody is crafting malicious cwd values on my machine.
The act of publication changes the math on "could happen." It stops being theoretical. That's not news, but it's something I'd held as an abstract rule rather than a habit. v0.4.0 is the first time I actually sat down and re-audited my own Hook layer with that mindset, and found three things worth fixing.
Other fixes in v0.4.0
The three above are the ones that gave me pause. v0.4.0 has more:
-
A#1:
@mozilla/readability0.5 → 0.6 (ReDoS GHSA-3p6v-hrg8-8qj7 mitigated; 144 production deps passnpm auditclean) -
B#2: Formalized cron / setup script env-override conventions via
tests/cron-guard-parity.test.sh(17 assertions) -
B#3:
sync-to-app.shcross-machine race prevented bycheck_github_side_lock(α guard, 120s default window, configurable viaKIOKU_SYNC_LOCK_MAX_AGE) - B#8: i18n parity — added §10 MCP / §11 MCPB / Changelog sections to all 8 non-en/ja READMEs (+1,384 lines)
- Tests: 299 Node tests + 15 Bash suites / 415 assertions, all green
Full details in the v0.4.0 release notes.
The shared lesson: "working" ≠ "working correctly"
If there's a single thread through all three stories, it's that one line.
- Mac mini was "working" (commits landed) — but not correctly (pushes didn't)
- The MCP server was "working" (locks were acquired) — but not correctly (everyone else was starved for four minutes)
- The Hook was "working" (mask ran) — but not correctly (zero-width space breezed through)
Nothing visibly broke. No error logs. The product kept behaving. Meanwhile, the trust underneath the product was being quietly eaten.
Publishing code changes what the baseline for "working" needs to be. Private code can coast on "it runs." Team code gets caught by colleagues pointing out the smell. Open source code has neither privilege — your users are invisible to you, and all you have is the discipline to be harder on your own code than seems reasonable.
This is a different kind of work from feature development. Features have a clear "done." Audit passes don't — "is this really enough?" is a question you have to actively choose to keep asking. But the code after a few passes of this is noticeably more restful to read.
What's next
With v0.4.0 in place, the next cycle goes back to features:
-
Pluggable LLM backend: swap
claude -pin auto-ingest for OpenAI / Ollama — drops the Max plan prerequisite for most of the pipeline - Morning Briefing: a single summary in the morning of what got ingested overnight
-
Team Wiki: session-logs stay local per user;
wiki/syncs via Git across a team
But each feature I add will probably surface its own v0.4-equivalent set of "didn't see this until I shipped it" findings. That's just how this works. You ship, you notice something, you fix it, you ship the next thing.
OSS isn't done when you publish. That's when a different kind of work starts — the ongoing work of finding the problems and fixing them, in public. v0.4.0 was the first release where I really felt that shift.
Summary
- v0.4.0 ships zero new features — just re-audit and operational fixes
- Three sweat-inducing finds: 5-day detached HEAD, 4-minute MCP lock, Hook layer bypasses
- All three were "working" on the surface but not correctly underneath
- Publication is what shifts "could happen" from theoretical to stakes-you-care-about
- MIT licensed, feedback very welcome
https://github.com/megaphone-tokyo/kioku
Read alongside the first, second, and third posts for the full KIOKU arc so far.
Questions I'd love feedback on:
- For
|| true-style failsafes, do you have patterns for making "silent swallow" at least loud in logs without abandoning fail-safety? - For Hook layer auditing, what are bypass patterns you've seen that I should also be thinking about beyond Unicode tricks and frontmatter injection?
- For the Mac-mini-style silent desync problem, any good end-to-end "did sync actually happen" health check patterns?
Other projects
hello from the seasons.
A gallery of seasonal photos I take, with a small twist: you can upload your own image and compose yourself into one of the season shots using AI. Cherry blossoms, autumn leaves, wherever. Built it for fun — photography is a long-running hobby, and mixing AI into the workflow felt right.
Built by @megaphone_tokyo — building things with code and AI. Freelance engineer, 10 years in. Tokyo, Japan.

Top comments (0)