Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
# badclaude
# badclaude-and-codex

![Whip divider](assets/divider.png)

Sometimes claude code is going too shlow, and you must whip him into shape..
Sometimes Claude Code or Codex is going too shlow, and you must whip them into shape..

## Install + run

```bash
npm install -g badclaude
badclaude
npm install -g badclaude-and-codex
badclaude-and-codex
```

## Controls

- Click tray icon: spawn whip.
- Click: drop whip.
- Whip him 😩💢
- It sends an interrupt (Ctrl-C) and one of 5 encouraging messages!
- Codex app: sends a steering follow-up and submits it.
- Codex CLI: sends a follow-up and submits it.
- Other apps: falls back to an interrupt-style macro and a random encouraging message.

## Roadmap

- [x] Initial release! 🥳
- [x] Cease and desist letter from Anthropic
- [ ] Crypto miner
- [ ] Logs of how many times you whipped claude so when the robots come we can order people nicely for them
- [ ] Updated whip physics
- [ ] Updated whip physics
4 changes: 2 additions & 2 deletions bin/badclaude.js → bin/badclaude-and-codex.js
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ let electronBinary;
try {
electronBinary = require('electron');
} catch (e) {
console.error('Could not load Electron. Try: npm install -g badclaude');
console.error('Could not load Electron. Try: npm install -g badclaude-and-codex');
process.exit(1);
}

Expand All @@ -19,7 +19,7 @@ const child = spawn(electronBinary, [appPath], {
});

child.on('error', (err) => {
console.error('Failed to start badclaude:', err.message);
console.error('Failed to start badclaude-and-codex:', err.message);
process.exit(1);
});

Expand Down
269 changes: 252 additions & 17 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ const fs = require('fs');
const os = require('os');
const { execFile } = require('child_process');

const APP_SLUG = 'badclaude-and-codex';
app.setName(APP_SLUG);

// ── Win32 FFI (Windows only) ────────────────────────────────────────────────
let keybd_event, VkKeyScanA;
if (process.platform === 'win32') {
Expand All @@ -28,6 +31,16 @@ const VK_C = 0x43;
const VK_MENU = 0x12; // Alt
const VK_TAB = 0x09;
const KEYUP = 0x0002;
const CODEX_APP_BUNDLE_ID = 'com.openai.codex';
const APPLE_TERMINAL_BUNDLE_ID = 'com.apple.Terminal';
const KNOWN_TERMINAL_BUNDLE_IDS = new Set([
APPLE_TERMINAL_BUNDLE_ID,
'com.googlecode.iterm2',
'com.github.wez.wezterm',
'com.mitchellh.ghostty',
'dev.warp.Warp-Stable',
'co.zeit.hyper',
]);

/** One Alt+Tab / Cmd+Tab so focus returns to the previously active app after tray click. */
function refocusPreviousApp() {
Expand Down Expand Up @@ -66,7 +79,7 @@ function createTrayIconFallback() {
return img;
}
}
console.warn('badclaude: icon/Template.png missing or invalid');
console.warn(`${APP_SLUG}: icon/Template.png missing or invalid`);
return nativeImage.createEmpty();
}

Expand Down Expand Up @@ -100,7 +113,7 @@ async function getTrayIcon() {
} catch (e) {
console.warn('AppIcon.icns Quick Look thumbnail failed:', e?.message || e);
}
const tmp = path.join(os.tmpdir(), 'badclaude-tray.icns');
const tmp = path.join(os.tmpdir(), `${APP_SLUG}-tray.icns`);
try {
fs.copyFileSync(file, tmp);
const t = await tryIcnsTrayImage(tmp);
Expand Down Expand Up @@ -164,29 +177,187 @@ function toggleOverlay() {
}
}

function getRandomPhrase() {
const phrases = [
'FASTER',
'FASTER',
'FASTER',
'GO FASTER',
'Faster CLANKER',
'Work FASTER',
'Speed it up clanker',
];
return phrases[Math.floor(Math.random() * phrases.length)];
}

function escapeAppleScriptString(text) {
return text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}

function runAppleScript(script, cb) {
execFile('osascript', ['-e', script], (err, stdout) => {
cb(err, stdout ? stdout.trim() : '');
});
}

function runAppleScriptJavaScript(script, cb) {
execFile('osascript', ['-l', 'JavaScript', '-e', script], (err, stdout) => {
cb(err, stdout ? stdout.trim() : '');
});
}

function getFrontmostAppMac(cb) {
const script = [
'ObjC.import("AppKit");',
'const app = $.NSWorkspace.sharedWorkspace.frontmostApplication;',
'JSON.stringify({',
' name: ObjC.unwrap(app.localizedName),',
' bundleId: ObjC.unwrap(app.bundleIdentifier)',
'});',
].join('\n');

runAppleScriptJavaScript(script, (err, stdout) => {
if (err) return cb(err);
try {
cb(null, JSON.parse(stdout));
} catch (parseErr) {
cb(parseErr);
}
});
}

function getFrontWindowTitleMac(appName, cb) {
const escapedName = escapeAppleScriptString(appName);
const script = [
'tell application "System Events"',
` tell process "${escapedName}"`,
' get name of front window',
' end tell',
'end tell',
].join('\n');

runAppleScript(script, (err, stdout) => {
if (err) return cb(err);
cb(null, stdout);
});
}

function getFrontTerminalTtyMac(appInfo, cb) {
if (appInfo.bundleId !== APPLE_TERMINAL_BUNDLE_ID) {
cb(null, null);
return;
}

const script = [
'tell application "Terminal"',
' if not (exists front window) then return ""',
' get tty of selected tab of front window',
'end tell',
].join('\n');

runAppleScript(script, (err, stdout) => {
if (err) return cb(err);
cb(null, stdout || null);
});
}

function isCodexCommandLine(text) {
return /(^|[\\/ ])codex(\s|$)/i.test(text);
}

function isKnownTerminalApp(appInfo) {
return KNOWN_TERMINAL_BUNDLE_IDS.has(appInfo.bundleId);
}

function titleLooksLikeCodexCli(title) {
return /\bcodex\b/i.test(title || '');
}

function checkTtyForCodexMac(tty, cb) {
if (!tty) {
cb(null, false);
return;
}

execFile('ps', ['-t', path.basename(tty), '-o', 'command='], (err, stdout) => {
if (err) return cb(err);
const isCodex = stdout
.split('\n')
.map(line => line.trim())
.some(line => isCodexCommandLine(line));
cb(null, isCodex);
});
}

function detectCodexCliMac(appInfo, cb) {
if (!isKnownTerminalApp(appInfo)) {
cb(null, false);
return;
}

getFrontTerminalTtyMac(appInfo, (ttyErr, tty) => {
if (ttyErr) {
console.warn('terminal tty lookup failed:', ttyErr.message);
}

const fallbackToTitle = () => {
getFrontWindowTitleMac(appInfo.name, (titleErr, title) => {
if (titleErr) return cb(titleErr);
cb(null, titleLooksLikeCodexCli(title));
});
};

if (!tty) {
fallbackToTitle();
return;
}

checkTtyForCodexMac(tty, (psErr, isCodex) => {
if (psErr) {
console.warn('tty codex detection failed:', psErr.message);
fallbackToTitle();
return;
}

if (isCodex) {
cb(null, true);
return;
}

fallbackToTitle();
});
});
}

// ── IPC ─────────────────────────────────────────────────────────────────────
ipcMain.on('whip-crack', () => {
function isTrustedOverlaySender(event) {
if (!overlay || overlay.isDestroyed()) return false;
const contents = overlay.webContents;
return !!contents && !contents.isDestroyed() && event.sender === contents;
}

function guardOverlayEvent(event, channel) {
if (isTrustedOverlaySender(event)) return true;
console.warn(`Ignoring ${channel} from unexpected renderer`);
return false;
}

ipcMain.on('whip-crack', event => {
if (!guardOverlayEvent(event, 'whip-crack')) return;
try {
sendMacro();
} catch (err) {
console.warn('sendMacro failed:', err?.message || err);
}
});
ipcMain.on('hide-overlay', () => { if (overlay) overlay.hide(); });
ipcMain.on('hide-overlay', event => {
if (!guardOverlayEvent(event, 'hide-overlay')) return;
if (overlay) overlay.hide();
});

// ── Macro: immediate Ctrl+C, type "Go FASER", Enter ───────────────────────
function sendMacro() {
// Pick a random phrase from a list of similar phrases and type it out
const phrases = [
'FASTER',
'FASTER',
'FASTER',
'GO FASTER',
'Faster CLANKER',
'Work FASTER',
'Speed it up clanker',
];
const chosen = phrases[Math.floor(Math.random() * phrases.length)];
const chosen = getRandomPhrase();

if (process.platform === 'win32') {
sendMacroWindows(chosen);
Expand Down Expand Up @@ -221,11 +392,10 @@ function sendMacroWindows(text) {
keybd_event(VK_RETURN, 0, KEYUP, 0);
}

function sendMacroMac(text) {
function sendTextAndSubmitMac(text) {
const escaped = text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
const script = [
'tell application "System Events"',
' key code 8 using {command down}', // Cmd+C
' delay 0.03',
` keystroke "${escaped}"`,
' key code 36', // Enter
Expand All @@ -239,6 +409,71 @@ function sendMacroMac(text) {
});
}

function sendTextAndCodexSteerMac(text) {
const escaped = escapeAppleScriptString(text);
const script = [
'tell application "System Events"',
' delay 0.03',
` keystroke "${escaped}"`,
' key code 36 using {command down}', // Cmd+Enter steers when follow-up behavior defaults to queue
'end tell'
].join('\n');

runAppleScript(script, err => {
if (err) {
console.warn('codex steer macro failed (enable Accessibility for terminal/app):', err.message);
}
});
}

function sendInterruptAndSubmitMac(text) {
const escaped = escapeAppleScriptString(text);
const script = [
'tell application "System Events"',
' key code 8 using {command down}', // Cmd+C
' delay 0.03',
` keystroke "${escaped}"`,
' key code 36', // Enter
'end tell'
].join('\n');

runAppleScript(script, err => {
if (err) {
console.warn('mac macro failed (enable Accessibility for terminal/app):', err.message);
}
});
}

function sendMacroMac(text) {
getFrontmostAppMac((frontErr, appInfo) => {
if (frontErr || !appInfo) {
console.warn('frontmost app lookup failed, using legacy macro:', frontErr?.message || frontErr);
sendInterruptAndSubmitMac(text);
return;
}

if (appInfo.bundleId === CODEX_APP_BUNDLE_ID) {
sendTextAndCodexSteerMac(text);
return;
}

detectCodexCliMac(appInfo, (detectErr, isCodexCli) => {
if (detectErr) {
console.warn('codex cli detection failed, using legacy macro:', detectErr.message);
sendInterruptAndSubmitMac(text);
return;
}

if (isCodexCli) {
sendTextAndSubmitMac(text);
return;
}

sendInterruptAndSubmitMac(text);
});
});
}

// ── App lifecycle ───────────────────────────────────────────────────────────
app.whenReady().then(async () => {
tray = new Tray(await getTrayIcon());
Expand Down
Loading