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:

  1. Lost on upgrade — you edit the gem source, then upgrade and your edits vanish.
  2. 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:

  1. Read meta.yml and resolve the method named in target.
  2. Recompute the fingerprint of the target method's current source (SHA256 over RubyVM::AbstractSyntaxTree.of(method)).
  3. Compare to the fingerprint stored in meta.yml:
    • Matchrequire patch.rb and 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" via on_mismatch: warn).

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 method
  • Const::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/ (when on_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:

  1. Run clacky patch_verify and note which patches got [DISABLED].
  2. For each disabled one, open ~/.clacky/patches/_disabled/<id>/patch.rb and keep it as a reference.
  3. Re-run patch_new to scaffold a fresh patch with the new fingerprint, then port the old logic.
  4. 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.yml or 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.