Runtime Patches
Need to fix or tweak a single method's behavior in the OpenClacky gem without forking the whole thing? The runtime patch layer lets you declare a prepend-style patch under ~/.clacky/patches/<id>/. Its standout feature is fingerprint validation: when you upgrade the gem and the upstream method's source changes, your patch is auto-disabled instead of getting silently applied to incompatible new code — which is the most dangerous failure mode.
This mechanism only supports method-level overrides (prepend a new method body). It deliberately does not support line-level / diff-style patches — those are the most reliable way to introduce subtle bugs.
Design rationale
Patches have two failure modes:
- Lost on upgrade — you edit the gem source, then upgrade and your edits vanish.
- Silently misapplied — an old patch lands on a new version of the method, looks fine, but actually breaks things.
The second is far more dangerous. This mechanism puts patches in user-land (solves #1) and records a source fingerprint of the target method per patch: at load time we recompute the fingerprint and compare. If they don't match, we disable by default (solves #2).
How it works
At startup, OpenClacky scans ~/.clacky/patches/<id>/ and processes each one:
- Read
meta.ymland resolve the method named intarget. - Recompute the fingerprint of the target method's current source (SHA256 over
RubyVM::AbstractSyntaxTree.of(method)). - Compare to the
fingerprintstored inmeta.yml:- Match →
require patch.rband the patch takes effect. - No match → by default, move the whole patch directory to
~/.clacky/patches/_disabled/and warn (configurable to "keep but don't apply" viaon_mismatch: warn).
- Match →
The result: after a gem upgrade, every patch that may no longer apply cleanly is automatically isolated — never silently misapplied.
Directory layout
~/.clacky/patches/
├── fix-search/
│ ├── meta.yml
│ └── patch.rb
└── _disabled/
└── stale-one/ # auto-disabled stale patches
└── ...
meta.yml fields
id: fix-search
description: bump web search timeout
target: "Clacky::Tools::WebSearch#execute" # # for instance, . for class
fingerprint: "a1b2c3d4..." # auto-filled by patch_new
gem_version: "0.7.0" # informational
on_mismatch: disable # disable (default) | warn
target syntax:
Const::Path#method— instance methodConst::Path.method— class method- No line numbers, file paths, regexes, or anything else.
CLI workflow
1. Scaffold (auto-computes fingerprint)
clacky patch_new fix-search "Clacky::Tools::WebSearch#execute" -d "bump timeout"
Creates ~/.clacky/patches/fix-search/:
meta.yml— fingerprint and current gem version pre-filled.patch.rb— a prepend-module skeleton; you only fill in the method body.
2. Write the patch
The skeleton (prepend means you can call super to reach the original):
module FixSearch
def execute(query:)
@timeout = 30
super
end
end
Clacky::Tools::WebSearch.prepend(FixSearch)
3. Verify status
clacky patch_verify
# or, equivalent:
clacky patch_list
Sample output:
[OK] fix-search
[DISABLED] old-fix — fingerprint mismatch — upstream code for X#y changed
[SKIP] bad-meta — meta.yml missing field: target
[OK]— loaded, prepend is in effect.[DISABLED]— moved to_disabled/(whenon_mismatch: disable); review the new upstream method, decide whether to rewrite the patch.[SKIP]— broken config (missing meta field, target unresolvable, …) — not loaded.
The command exits non-zero if anything is [SKIP].
Post-upgrade workflow
Strongly recommended after every gem upgrade:
- Run
clacky patch_verifyand note which patches got[DISABLED]. - For each disabled one, open
~/.clacky/patches/_disabled/<id>/patch.rband keep it as a reference. - Re-run
patch_newto scaffold a fresh patch with the new fingerprint, then port the old logic. - Delete
_disabled/<id>/once you're done.
on_mismatch options
| Value | Behavior |
|---|---|
disable (default) |
Move to _disabled/, don't apply, warn. |
warn |
Stay in place, don't apply, warn. Useful while debugging. |
There is no "force apply" option. That's deliberate — force-applying a mismatched patch is exactly what this mechanism exists to prevent.
When not to use a patch
- Adding a new method or class — there's no drift risk. Don't bother with the patch system; write a Skill in
~/.clacky/skills/or monkey-patch from your project. - Changing a constant or default config value — use
~/.clacky/config.ymlor env vars, don't touch code. - A large cross-method refactor — fork the gem or send a PR. Stacking many patches makes upgrades blow up at scale.
The sweet spot for this mechanism: one very specific method, one very local fix.