Skip to content

Instantly share code, notes, and snippets.

@Wowfunhappy
Created July 23, 2026 02:45
Show Gist options
  • Select an option

  • Save Wowfunhappy/002009cf9c410ed2f751bde8e47db483 to your computer and use it in GitHub Desktop.

Select an option

Save Wowfunhappy/002009cf9c410ed2f751bde8e47db483 to your computer and use it in GitHub Desktop.
Claude Code: Enter creates newlines instead of submitting your message
// Claude Code: tap Enter for a newline, Option+Enter for submit.
//
// Tap Enter -> insert a newline
// Option+Enter -> submit (convenient locally)
//
// Alternately, you can also hold enter to submit. Does not work in /tui fullscreen.
//
// Usage — Claude Code is a Bun-compiled binary, so it honors Bun's --preload:
//
// BUN_OPTIONS="--preload=/path/to/claude-code-enter-newline.js" claude
//
// (Put that in a shell alias or wrapper to make it permanent.)
//
// How it works: Claude submits only on a bare Return (0x0d) with no meta; a
// Return carrying meta inserts a newline instead. So this rewrites stdin bytes
// before Claude's key parser sees them: a tapped 0x0d becomes 0x1b 0x0d
// (meta+Return = newline), and Option+Enter's 0x1b 0x0d becomes a bare 0x0d.
//
// Interception point: Claude (Ink on Bun) reads stdin in PAUSED mode — it waits
// for 'readable' and pulls bytes with stdin.read(), and never registers a 'data'
// listener. So we wrap stdin.read(). A 'data' hook is kept as a fallback for any
// flowing-mode consumer, with its own state so the two can't interfere.
//
// Verified against Claude Code 2.1.215 / 2.1.216 on macOS 10.9 Mavericks.
// Public domain / CC0 — do whatever you like with it.
// This code was, naturally, written by Claude, specifically Opus 4.8.
(function () {
"use strict";
var ESC = 0x1b, CR = 0x0d, LB = 0x5b, BS = 0x7f; // ESC, '\r', '[', DEL/backspace
// Timing thresholds (ms), env-tunable for live adjustment. Auto-repeat over
// VNC measured ~110ms between bytes; local repeat is faster still. A tap or a
// deliberate double-tap is slower than FAST_GAP, so it never looks like a hold.
function envInt(name, dflt) { var v = parseInt(process.env[name], 10); return (v > 0) ? v : dflt; }
var FAST_GAP = envInt("MF_KB_FASTGAP", 180); // gap <= this => part of an auto-repeat burst
var NEED_FAST = envInt("MF_KB_NEEDFAST", 5); // this many consecutive fast gaps confirms a HOLD
var RUN_END = envInt("MF_KB_RUNEND", 700); // gap > this ends a run / the post-submit swallow
function now() { return Date.now(); }
function createRemapper(log) {
var PASTE_START = Buffer.from("\x1b[200~");
var PASTE_END = Buffer.from("\x1b[201~");
var inPaste = false;
// Hold-detection state.
var lastCR = 0; // timestamp of previous Return
var runNewlines = 0; // newlines emitted in the current contiguous Return run
var fastCount = 0; // consecutive fast gaps seen
var swallowing = false; // after a hold-submit, eat the rest of the held burst
function marker(buf, i, m) {
if (i + m.length > buf.length) return false;
for (var k = 0; k < m.length; k++) if (buf[i + k] !== m[k]) return false;
return true;
}
function transform(buf) {
var out = [];
var i = 0, n = buf.length;
while (i < n) {
// Bracketed paste: pass through untouched, don't let it affect hold state.
if (buf[i] === ESC && buf[i + 1] === LB) {
if (marker(buf, i, PASTE_START)) { inPaste = true; for (var a = 0; a < PASTE_START.length; a++) out.push(buf[i + a]); i += PASTE_START.length; continue; }
if (marker(buf, i, PASTE_END)) { inPaste = false; for (var b = 0; b < PASTE_END.length; b++) out.push(buf[i + b]); i += PASTE_END.length; continue; }
}
if (inPaste) { out.push(buf[i]); i += 1; continue; }
// Option+Enter (ESC+CR in one keystroke) -> submit. Resets hold state.
if (buf[i] === ESC && buf[i + 1] === CR) {
out.push(CR);
runNewlines = 0; fastCount = 0; swallowing = false;
if (log) log("optEnter -> submit");
i += 2; continue;
}
if (buf[i] === CR) {
var t = now(), gap = t - lastCR; lastCR = t;
if (swallowing) {
if (gap <= RUN_END) { if (log) log("swallow held CR gap=" + gap); i += 1; continue; }
swallowing = false; runNewlines = 0; fastCount = 0; // burst ended; fresh Return
}
if (gap > RUN_END) { runNewlines = 0; fastCount = 0; }
else if (gap <= FAST_GAP) fastCount += 1;
else fastCount = 0;
if (fastCount >= NEED_FAST) {
// HOLD confirmed: erase this run's newlines, then submit once.
for (var u = 0; u < runNewlines; u++) out.push(BS);
out.push(CR);
if (log) log("HOLD -> undo " + runNewlines + " nl + submit (gap=" + gap + ")");
runNewlines = 0; fastCount = 0; swallowing = true;
} else {
out.push(ESC, CR); // tap (so far) -> newline
runNewlines += 1;
if (log) log("CR -> newline gap=" + gap + " fast=" + fastCount + " run=" + runNewlines);
}
i += 1; continue;
}
// Any other byte breaks the pure-newline run so we never undo across it.
runNewlines = 0; fastCount = 0;
out.push(buf[i]); i += 1;
}
return Buffer.from(out);
}
return { transform: transform };
}
var stdin = process.stdin;
if (!stdin || !stdin.isTTY) return;
if (process.env.MF_KEYBIND_DISABLE) return; // escape hatch: behave like stock Claude
var dbg = null;
if (process.env.MF_KEYBIND_DEBUG) {
try {
var fs = require("fs"), logPath = process.env.MF_KEYBIND_DEBUG;
dbg = function (m) { try { fs.appendFileSync(logPath, m + "\n"); } catch (e) {} };
dbg("[enter-newline] installed on tty");
} catch (e) {}
}
function hexOf(b) { return Array.prototype.map.call(b, function (x) { return ("0" + x.toString(16)).slice(-2); }).join(" "); }
function coerce(chunk, remap) {
var wasString = typeof chunk === "string";
var b = wasString ? Buffer.from(chunk, "utf8") : chunk;
if (!Buffer.isBuffer(b)) return chunk;
b = remap.transform(b);
return wasString ? b.toString("utf8") : b;
}
// Primary path: Claude pulls input via stdin.read() in paused mode.
var readRemap = createRemapper(dbg);
var realRead = stdin.read;
if (typeof realRead === "function") {
stdin.read = function (size) {
var chunk = realRead.apply(this, arguments);
if (chunk == null || size != null) return chunk;
return coerce(chunk, readRemap);
};
}
// Fallback: any flowing-mode consumer using 'data' events (own remapper state).
var emitRemap = createRemapper(null);
var realEmit = stdin.emit;
stdin.emit = function (event, chunk) {
if (event === "data" && this.listenerCount("data") > 0) {
return realEmit.call(this, event, coerce(chunk, emitRemap));
}
return realEmit.apply(this, arguments);
};
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment