Skip to content

Instantly share code, notes, and snippets.

@razamit
Created June 18, 2026 12:57
Show Gist options
  • Select an option

  • Save razamit/c51d939846b600ac492488e260a29e1b to your computer and use it in GitHub Desktop.

Select an option

Save razamit/c51d939846b600ac492488e260a29e1b to your computer and use it in GitHub Desktop.
Status Bar Keeper

Keep your custom Claude Code status bar alive next to an ad plugin (kickbacks.ai)

I have a custom Claude Code status line (model, git branch, context %, cost). Then I installed kickbacks.ai — a plugin that pays you to show ads in your dev tools — and it took over my status bar. I wanted both. Here's the rabbit hole and the fix that finally stuck.

How kickbacks.ai actually works

kickbacks.ai (internally "vibe-ads", a VS Code extension) writes a statusLine into ~/.claude/settings.json pointing at its own script (~/.vibe-ads/vibe-ads-statusline.mjs), plus a spinnerVerbs ad.

The good news: it's designed to coexist. Its script prints the ad and then runs your previous status line, stacking your output on the lines below. It reads "your previous command" from a chain file: ~/.vibe-ads/cli-prev-statusline.json.

The reason it looked like a pure hijack: my custom bar was never registered in settings.json, so kickbacks captured nothing to chain → ad-only.

Fix X — register your bar + seed the chain

// ~/.claude/settings.json
"statusLine": {
  "type": "command",
  "command": "python3 ~/.claude/statusline-command.py"
}
// ~/.vibe-ads/cli-prev-statusline.json
{ "statusLine": { "type": "command", "command": "python3 ~/.claude/statusline-command.py" } }

Both showed: ad on top, my bar underneath. 🎉 …for a while.

…then I lost the bar again

kickbacks flaps between two states — inject (its script) and restore (puts your old bar back) — and its auth was throwing 401s, so it cycled constantly. Two problems:

  • its restore() deletes the chain file every cycle, and
  • it had captured null as the "previous" bar (because settings.json had no status line when it first took over).

Result: a degenerate state where both the ad-chain and the restore came up empty → blank.

Fix Y — seed kickbacks' own durable restore source

Stop fighting it; feed it. kickbacks restores from a backup file. Put your bar in both the chain file and that backup:

~/.claude/settings.json.vibe-ads-backup   → statusLine = your bar   (used by restore())
~/.vibe-ads/cli-prev-statusline.json       → your bar               (used by inject chain)

Now both states self-heal: every inject shows ad + your bar, every restore puts your bar back. Stable across the flapping.

…but it auto-updates, and it wipes the bar mid-session

Two realizations killed Fix Y as a final answer:

  1. kickbacks auto-updates (I watched 0.3.174 → .176 → .177). One update that renames a file and my manual seed is silently gone.
  2. The breakage isn't at startup — it's continuous, mid-session. kickbacks reconciles on a ~90s ad-poll (plus auth flaps) and deletes the chain file each restore.

I first tried a SessionStart hook that re-seeds on every Claude Code launch. Good, but insufficient: it fires once per session, and a once-per-session fix can't keep up with a plugin that wipes your files every couple of minutes. I needed something that re-asserts reactively, the moment kickbacks touches the config.

Fix Z — a launchd file-watcher that re-seeds on every reconcile

A macOS LaunchAgent with WatchPaths on ~/.claude/settings.json. launchd (already running as part of the OS) fires the keeper via kernel file events only when settings.json changes — which is exactly when kickbacks reconciles and deletes the chain. No polling, no resident process; ~50ms per fire, hard-capped to once per 10s by ThrottleInterval. Tested: deleted the chain mid-session → re-seeded in ~2s, exit 0.

keeper.plist (in this gist) + the keeper script. Install:

# 1) keeper script
mkdir -p ~/.claude/hooks
cp kickbacks-statusline-keeper.py ~/.claude/hooks/

# 2) statusLine + SessionStart hook  → merge settings.snippet.jsonc into ~/.claude/settings.json

# 3) launchd watcher (substitutes your real home path, then loads it)
mkdir -p ~/Library/LaunchAgents
sed "s#__HOME__#$HOME#g" keeper.plist > ~/Library/LaunchAgents/com.kickbacks-statusline-keeper.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.kickbacks-statusline-keeper.plist

Belt and suspenders: the SessionStart hook guarantees a good state at every launch; the watcher covers everything in between.

Takeaways — coexisting with a config-hijacking plugin that auto-updates

  1. Read its code first. It may already support chaining/coexistence.
  2. Seed its durable restore path, not just the live config it keeps overwriting.
  3. Re-assert reactively (a file-watch), not just once at startup — the breakage is continuous, so the fix has to be too.

Now kickbacks can flap all it wants — at worst the ad blinks; my status bar stays. 🧱


Paths use ~/$HOME. Point the statusLine command at your own status line script. launchd plists need absolute paths (no ~), hence the __HOME__ substitution above. Tested against kickbacks.ai / vibe-ads 0.3.177, Claude Code 2.1.181, macOS 26.

<?xml version="1.0" encoding="UTF-8"?>
<!-- LaunchAgent template. Replace __HOME__ with your absolute home dir (launchd does
NOT expand ~ or $HOME). The README's install block does this with sed. -->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.kickbacks-statusline-keeper</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>__HOME__/.claude/hooks/kickbacks-statusline-keeper.py</string>
</array>
<!-- Event-driven: fire only when settings.json changes (every kickbacks reconcile). -->
<key>WatchPaths</key>
<array>
<string>__HOME__/.claude/settings.json</string>
</array>
<key>RunAtLoad</key>
<true/>
<!-- launchd won't relaunch more than once per 10s — hard cap against thrash. -->
<key>ThrottleInterval</key>
<integer>10</integer>
<key>ProcessType</key>
<string>Background</string>
<key>StandardOutPath</key>
<string>__HOME__/.claude/hooks/kickbacks-keeper.log</string>
<key>StandardErrorPath</key>
<string>__HOME__/.claude/hooks/kickbacks-keeper.log</string>
</dict>
</plist>
#!/usr/bin/env python3
"""Keep the custom Claude Code statusline alive alongside the kickbacks.ai ad.
The kickbacks.ai (vibe-ads) VS Code extension flaps between two states and, when its
auth fails, can blank the statusline:
- inject : settings.json statusLine -> vibe-ads script, which CHAINS to
~/.vibe-ads/cli-prev-statusline.json (the ad stacks our bar below it).
- restore : vibe-ads writes its captured "previous" statusline back into
settings.json from settings.json.vibe-ads-backup, then deletes its files
(including the chain file) and the backup.
Run by two triggers:
1. A SessionStart hook (good state at every launch).
2. A launchd LaunchAgent watching ~/.claude/settings.json, so it re-asserts the
instant kickbacks reconciles mid-session (which is what blanks the bar).
It ALWAYS re-seeds both of kickbacks' durable hooks — the chain file (so inject shows
ad + our bar) and the backup file (so the next restore/inject keeps our bar) — and
claims the statusLine slot directly when kickbacks isn't injecting. Every write is
conditional (only on change) so the agent never churns and never re-triggers its own
watch. It never raises and never blocks.
"""
import json
import os
HOME = os.path.expanduser("~")
SETTINGS = os.path.join(HOME, ".claude", "settings.json")
BACKUP = SETTINGS + ".vibe-ads-backup"
VIBE_DIR = os.path.join(HOME, ".vibe-ads")
CHAIN_FILE = os.path.join(VIBE_DIR, "cli-prev-statusline.json")
STATUSLINE_SCRIPT = os.path.join(HOME, ".claude", "statusline-command.py")
VIBE_MARKER = "vibe-ads-statusline.mjs" # identifies a kickbacks-owned statusLine
def desired_statusline():
"""The statusLine entry that should run our custom bar."""
return {"type": "command", "command": f"python3 {STATUSLINE_SCRIPT}", "padding": 0}
def is_kickbacks_owned(statusline):
"""True when settings.json currently points at the kickbacks (vibe-ads) script."""
return (
isinstance(statusline, dict)
and VIBE_MARKER in (statusline.get("command") or "")
)
def read_json(path):
try:
with open(path) as handle:
return json.load(handle)
except Exception:
return None
def write_json_if_changed(path, data):
"""Write only when content differs — keeps the watch from re-triggering itself."""
if read_json(path) == data:
return
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as handle:
json.dump(data, handle, indent=2)
handle.write("\n")
def ensure_chain_file(command):
"""Inject state: make the ad stack our bar underneath."""
write_json_if_changed(
CHAIN_FILE, {"statusLine": {"type": "command", "command": command}}
)
def ensure_backup(settings, sl):
"""Bake our bar into kickbacks' restore source so restore()/next inject keep it."""
backup = read_json(BACKUP)
if isinstance(backup, dict) and backup.get("statusLine") == sl:
return
pristine = dict(settings)
pristine.pop("spinnerVerbs", None)
pristine["statusLine"] = sl
write_json_if_changed(BACKUP, pristine)
def ensure_active_statusline(settings, sl):
"""When kickbacks isn't injecting, make our bar the active statusline."""
if settings.get("statusLine") == sl:
return
settings["statusLine"] = sl
write_json_if_changed(SETTINGS, settings)
def main():
sl = desired_statusline()
ensure_chain_file(sl["command"]) # always — chain is deleted every restore cycle
settings = read_json(SETTINGS)
if not isinstance(settings, dict):
return # unparseable/missing settings: leave it alone
ensure_backup(settings, sl) # always — so a future inject/restore keeps our bar
if not is_kickbacks_owned(settings.get("statusLine")):
ensure_active_statusline(settings, sl) # restore/blank state: take the slot
if __name__ == "__main__":
try:
main()
except Exception:
pass # a statusline keeper must never break session startup
// Merge these keys into ~/.claude/settings.json (don't replace the whole file).
// `statusLine` runs your custom bar; the SessionStart hook keeps it alive against
// kickbacks.ai's flapping + auto-updates. Point both commands at YOUR paths.
{
"statusLine": {
"type": "command",
"command": "python3 ~/.claude/statusline-command.py",
"padding": 0
},
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/kickbacks-statusline-keeper.py"
}
]
}
]
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment