Block cursor in vim normal mode, thin bar in insert mode

Changes the terminal cursor with Claude Code's vim mode: a steady block in normal mode, a steady thin bar in insert mode.
/config → Editor mode → vim).ESC [ N q). Most do.sh and ps)./plugin install vim-cursor --marketplace nuTTeLLo/claude-mods
Nothing to configure. To remove it: /plugin uninstall vim-cursor.
-- INSERT -- while in insert mode. Anything else counts as normal.-- INSERT -- wording; a Claude Code release that changes it leaves the cursor a block.sh, so the change lags the keypress slightly.Edit the codes at the top of hooks/register.ts: 1/2 block, 3/4 underline, 5/6 bar (odd blinks, even is steady).
hooks/register.ts 70 lines1import type { EngineInterface, Register } from 'claude-code'
2
3// DECSCUSR shapes: the terminal's own cursor, steady.
4const BLOCK = '2'
5const BAR = '6'
6const DEFAULT = '0'
7
8// Hooks run without a controlling terminal, so walk up to the first ancestor that has one.
9const FIND_TTY = `p=$PPID
10while [ -n "$p" ] && [ "$p" != 1 ]; do
11 t=$(ps -o tty= -p "$p" | tr -d ' ')
12 case "$t" in
13 ''|'??') p=$(ps -o ppid= -p "$p" | tr -d ' ') ;;
14 *) echo "/dev/$t"; exit 0 ;;
15 esac
16done
17exit 1`
18
19let tty: string | undefined
20let shape: string | undefined
21
22// The hint line under the prompt carries vim's `-- INSERT --` marker.
23export function shapeFor(hint: string): string {
24 return /insert/i.test(hint) ? BAR : BLOCK
25}
26
27async function findTty($: EngineInterface): Promise<string | undefined> {
28 if (tty !== undefined) return tty
29 try {
30 const ran = await $.process.run(['sh', '-c', FIND_TTY])
31 const found = ran.stdout.trim()
32 if (ran.exitCode === 0 && found.startsWith('/dev/')) tty = found
33 } catch {
34 // No terminal to write to (a desktop or headless session): the mod does nothing.
35 }
36
37 return tty
38}
39
40async function setShape($: EngineInterface, wanted: string): Promise<void> {
41 if (wanted === shape) return
42 const device = await findTty($)
43 if (device === undefined) return
44 shape = wanted
45 await $.process.run(['sh', '-c', 'printf "\\033[%s q" "$1" > "$2"', 'sh', wanted, device])
46}
47
48export const register: Register = on => {
49 on('session.start', async ($, e, next) => {
50 // Vim mode starts a prompt in insert.
51 void setShape($, BAR)
52
53 return next(e)
54 })
55
56 on('ui.render', { component: 'PromptHint' }, ($, e, next) => {
57 if (e.surface === 'terminal') {
58 void setShape($, shapeFor(e.props.hint))
59 }
60
61 return next(e)
62 })
63
64 on('session.end', async ($, e, next) => {
65 await setShape($, DEFAULT)
66
67 return next(e)
68 })
69}
70