Steer Claude mid-turn with one chord. /steering-wheel sets Ctrl+X Ctrl+X to send a waiting message right away, and the hint under the prompt says so.

Steer Claude mid-turn with one chord.
When Claude is working and you have typed your next message, Claude Code holds it until the turn ends. Steering Wheel lets you send it right away and steer the work. Press Ctrl+X, then Ctrl+X again.
/steering-wheel adds Ctrl+X Ctrl+X to your own ~/.claude/keybindings.json. That chord then sends a waiting message now.Versions 0.1.1 and before used F5. Claude Code does not take function keys, so F5 did nothing. Version 0.1.0 used Alt+Enter, which Windows Terminal keeps for full screen. Ctrl+X Ctrl+X works in any terminal.
If an older version left F5 in your file, or switched Ctrl+X Ctrl+S off, /steering-wheel takes those two lines back out. It touches nothing else.
/steering-wheel.MIT licensed. Made by Rostech.
See every Rostech mod, live, at rostech.app.
hooks/register.ts 134 lines1import type { Register } from 'claude-code'
2
3// 🛞 STEERING WHEEL — one chord to send now, so you can steer Claude mid-turn. Renamed from
4// Ignition on 3 Oct, his words: "rename ignition to steering wheel because GPT uses steer".
5// Ignition itself came back as Horn on 4 Oct, his words: "i want to
6// change ignition to 'Horn' because when I bash F5 its like im honking the horn".
7//
8// Why not F5 any more: Claude Code's keybindings only take letters, digits and the named
9// special keys (https://code.claude.com/docs/en/keybindings). F5 is not one, so the binding
10// was skipped without a word. His words: "F5 never works. Tapping ESC is the only thing that
11// works". He picked Ctrl+X Ctrl+X: "can we do ctrl x ctrl x" → "seems to work". It is not a
12// default chord in the Chat context, so binding it takes nothing away.
13//
14// A mod cannot bind keys. The key lives in each person's own ~/.claude/keybindings.json,
15// so /steering-wheel writes the chord there, merged. It never switches off a default. It also takes
16// back the two lines an older version could have left behind: F5 → send now, and the
17// default Ctrl+X Ctrl+S switched off. Every other line stays as the person had it.
18// The hint names whatever key the file says now, read off the file, never typed here.
19
20const ACTION = 'chat:sendNow'
21const KEY = 'ctrl+x ctrl+x'
22const DEFAULT_CHORD = 'ctrl+x ctrl+s'
23const NAME = 'steering-wheel'
24
25type Block = { context: string; bindings: Record<string, string | null> }
26type File = { $schema?: string; $docs?: string; bindings: Block[] }
27
28// A function key: "f5", "shift+f5". Claude Code does not take these.
29export function isFunctionKey(chord: string): boolean {
30 return chord.split(' ').some(stroke => /^f\d+$/i.test(stroke.split('+').pop() ?? ''))
31}
32
33// "ctrl+x ctrl+s" → "Ctrl+X Ctrl+S", "f5" → "F5", "alt+enter" → "Alt+Enter"
34export function pretty(chord: string): string {
35 const part = (p: string): string =>
36 p.length === 1 || /^f\d+$/.test(p) ? p.toUpperCase() : p[0].toUpperCase() + p.slice(1)
37 return chord
38 .split(' ')
39 .map(stroke => stroke.split('+').map(part).join('+'))
40 .join(' ')
41}
42
43// The key that sends now: the first one the file binds to the action that can actually fire
44// (never a function key), else the default chord unless the file switched it off.
45export function sendKey(file: File | null): string | null {
46 let defaultOff = false
47 for (const block of file?.bindings ?? []) {
48 if (block.context !== 'Chat') continue
49 for (const [key, action] of Object.entries(block.bindings)) {
50 if (action === ACTION && !isFunctionKey(key)) return key
51 if (key === DEFAULT_CHORD && action === null) defaultOff = true
52 }
53 }
54 return defaultOff ? 'ctrl+enter' : DEFAULT_CHORD
55}
56
57// The lines an older version wrote that must come back out: F5 → send now, and the default
58// chord switched off. Only those two, only in the Chat block.
59export function leftovers(file: File | null): string[] {
60 const chat = file?.bindings?.find(b => b.context === 'Chat')
61 if (!chat) return []
62 const out: string[] = []
63 if (chat.bindings.f5 === ACTION) out.push('f5')
64 if (DEFAULT_CHORD in chat.bindings && chat.bindings[DEFAULT_CHORD] === null) out.push(DEFAULT_CHORD)
65 return out
66}
67
68// "… · ctrl+x ctrl+s to send now · …" → "… · tap Ctrl+X Ctrl+X to send now · …"
69export function rewrite(hint: string, key: string): string {
70 return hint.replace(/(?:[^\s·]+\s){0,2}to send now/i, `tap ${pretty(key)} to send now`)
71}
72
73// Adds the chord to the Chat block and takes the leftovers out. Merges: every other binding
74// stays as the person had it.
75export function withKey(file: File | null): File {
76 const out: File = file
77 ? { ...file, bindings: file.bindings.map(b => ({ ...b, bindings: { ...b.bindings } })) }
78 : {
79 $schema: 'https://www.schemastore.org/claude-code-keybindings.json',
80 $docs: 'https://code.claude.com/docs/en/keybindings',
81 bindings: [],
82 }
83 let chat = out.bindings.find(b => b.context === 'Chat')
84 if (!chat) {
85 chat = { context: 'Chat', bindings: {} }
86 out.bindings.push(chat)
87 }
88 for (const key of leftovers(out)) delete chat.bindings[key]
89 chat.bindings[KEY] = ACTION
90 return out
91}
92
93export function parse(text: string | null): File | null {
94 if (text === null) return null
95 try {
96 return JSON.parse(text) as File
97 } catch {
98 return null
99 }
100}
101
102export const register: Register = on => {
103 let key: string = DEFAULT_CHORD
104
105 on('session.start', async ($, e, next) => {
106 const home = (await $.env.get('USERPROFILE')) || (await $.env.get('HOME')) || ''
107 const path = `${home}/.claude/keybindings.json`
108 const text = (await $.fs.exists(path)) ? await $.fs.read(path) : null
109 key = sendKey(parse(text)) ?? DEFAULT_CHORD
110 await $.command.register({ name: NAME, description: 'Set Ctrl+X Ctrl+X to send a waiting message now' })
111 return next(e)
112 })
113
114 on('command.run', { command: NAME }, async ($, e) => {
115 const home = (await $.env.get('USERPROFILE')) || (await $.env.get('HOME')) || ''
116 const path = `${home}/.claude/keybindings.json`
117 const before = parse((await $.fs.exists(path)) ? await $.fs.read(path) : null)
118 const old = leftovers(before)
119 if (sendKey(before) === KEY && old.length === 0) {
120 key = KEY
121 return { text: `Ctrl+X Ctrl+X already sends now. Nothing changed. (${path})` }
122 }
123 await $.fs.write(path, JSON.stringify(withKey(before), null, 2) + '\n')
124 key = KEY
125 const fixed = old.length ? ` Took out what an older version left: ${old.map(pretty).join(', ')}.` : ''
126 return { text: `Ctrl+X Ctrl+X now sends a waiting message right away. Ctrl+X Ctrl+S still works too.${fixed} (${path})` }
127 })
128
129 on('ui.render', { component: 'PromptHint' }, ($, e, next) => {
130 if (!/send now/i.test(e.props.hint)) return next(e)
131 return next({ ...e, props: { ...e.props, hint: rewrite(e.props.hint, key) } })
132 })
133}
134