Cursor使用Hook使用系统的Node执行命令

前言

Cursor在执行命令的时候会使用内置的Node,对应版本是22.22.0

这就导致了如果项目使用的不是这个版本,就会导致AI检测的不对。

这里可以使用Hook进行命令的封装,让使用系统环境的Node。

步骤

脚本

.cursor/hooks/pre-tool-shell-node.mjs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// preToolUse Shell: Win → PowerShell (chcp 65001 + UTF-8 + registry Path) then cmd; Unix → path_helper. Debug: CURSOR_HOOK_DEBUG=1.

import { Buffer } from 'node:buffer'
import { execSync } from 'node:child_process'
import process from 'node:process'

const W = process.platform === 'win32'

const stdin = () =>
new Promise((r) => {
let d = ''
process.stdin.setEncoding('utf8')
process.stdin.on('data', (c) => (d += c))
process.stdin.on('end', () => r(d))
process.stdin.on('error', () => r(d))
})

const out = (o) => void process.stdout.write(`${JSON.stringify(o)}\n`)
const log = (m) => {
if (process.env.CURSOR_HOOK_DEBUG) process.stderr.write(`[pre-tool-shell-node] ${m}\n`)
}

function toWin(s) {
s = String(s ?? '').trim()
if (!s || !W) return s
const m = s.match(/^\/([a-zA-Z]):\/(.*)$/)
if (m) return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, '\\')}`
return /^[a-zA-Z]:\//.test(s) ? s.replace(/\//g, '\\') : s
}

function cwdOf(inp, ti) {
for (const v of [
ti.cwd,
ti.working_directory,
ti.workingDirectory,
inp.cwd,
inp.working_directory,
inp.workingDirectory,
inp.workspace_roots?.[0],
inp.workspaceRoots?.[0],
]) {
const t = v == null ? '' : String(v).trim()
if (t) return toWin(t)
}
try {
return toWin(process.cwd())
} catch {
return ''
}
}

function wrapWin(cd, cmd) {
const q = cd.replace(/'/g, "''")
const ps = [
'try{cmd.exe /c "chcp 65001>nul"|Out-Null}catch{}',
'$u=[System.Text.UTF8Encoding]::new($false)',
'[Console]::OutputEncoding=$u;[Console]::InputEncoding=$u;$OutputEncoding=$u',
"$env:PYTHONUTF8='1';$env:PYTHONIOENCODING='utf-8'",
"$env:Path=[Environment]::GetEnvironmentVariable('Path','Machine')+';'+[Environment]::GetEnvironmentVariable('Path','User')",
`Set-Location -LiteralPath '${q}'`,
cmd,
].join(';')
return `powershell.exe -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(ps, 'utf16le').toString('base64')}`
}

function wrapUnix(cd, cmd) {
const q = (s) => s.replace(/'/g, "'\\''")
try {
const o = execSync('/usr/libexec/path_helper -s', { encoding: 'utf8', timeout: 3000 })
const p = o.match(/PATH="([^"]+)"/)?.[1]
if (p) return `export PATH='${q(p)}' && cd '${q(cd)}' && ${cmd}`
} catch {}
return `cd '${q(cd)}' && ${cmd}`
}

let inp = {}
try {
inp = JSON.parse((await stdin()).replace(/^\uFEFF/, '').trim() || '{}')
} catch {}

const name = inp.tool_name ?? inp.toolName ?? ''
if (!/^shell$/i.test(name) && name !== 'Bash') {
log(`skip ${name || '?'}`)
out({ permission: 'allow' })
process.exit(0)
}

const ti = inp.tool_input ?? inp.toolInput ?? {}
const cmd = ti.command ?? inp.command ?? ''
const cd = cwdOf(inp, ti)
if (!cmd || !cd) {
log(`skip !cmd=${!cmd} !cd=${!cd}`)
out({ permission: 'allow' })
process.exit(0)
}

log(`${W ? 'win' : 'unix'} ${cd}`)
out({
permission: 'allow',
updated_input: { ...ti, command: W ? wrapWin(cd, cmd) : wrapUnix(cd, cmd), cwd: cd, working_directory: cd },
})

原理就是

把要执行的命令使用powershell.exe封装了下进行调用。

要注意编码,否则会中文乱码。

配置

.cursor/hooks.json

1
2
3
4
5
6
7
8
9
10
11
12
{
"version": 1,
"hooks": {
"preToolUse": [
{
"command": "node .cursor/hooks/pre-tool-shell-node.mjs",
"matcher": "Shell"
}
]
}
}