Files
orca/src/main/jcode/hook-command-quoting.test.ts
T
Neilandczzczz 4c2cb3cf12 fix(jcode): quote the managed hook path, drop tools from patch prompts
Three fixes, all on paths this PR could not exercise locally.

The managed hook command was stored as a bare path. jcode tokenizes that
string shell-style before exec'ing it directly (parse_hook_command,
crates/jcode-terminal-launch/src/lib.rs): unquoted whitespace splits, and
every unquoted backslash is consumed as an escape. So on Windows
`C:\Users\me\.orca\agent-hooks\jcode-hook.cmd` reached exec as
`C:Usersme.orcaagent-hooksjcode-hook.cmd` and no hook fired at all, and a
POSIX home with a space split into two arguments. Store the path
single-quoted (verbatim, backslashes included), falling back to double
quotes for a path containing a single quote. Existing bare entries are
already repointed by the stale-key path, and getStatus accepts both forms
so the repair is not reported as a user-owned hook. The quoting helper was
previously dead code that only tests called; the three production sites
now use it. isJcodeManagedCommand also normalizes separators, since a
`/`-only needle never matched a Windows entry.

Commit-message generation feeds a staged patch to `jcode run` as the
prompt — attacker-influenced text — while jcode's default profile exposes
shell, read, write, and MCP. Pass `--tool-profile none`, which resolves to
an empty allowed-tool set in jcode's config (base_allowed_tools), matching
the read-only posture claude (plan) and codex (read-only) already take.

docs/reference/jcode-hook-events.md was never actually in this PR: the
repo ignores docs/** and tracks reference docs by allow-list only, so the
captured-payload evidence four source comments point at was silently
dropped. Allow-list it.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>
2026-10-03 01:49:40 -07:00

82 lines
2.6 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { getJcodeManagedCommand, isJcodeManagedCommand } from './hook-settings'
/**
* jcode tokenizes a configured hook command shell-style before executing it directly
* (`parse_hook_command`, crates/jcode-terminal-launch/src/lib.rs). These cases mirror
* that tokenizer: unquoted whitespace splits, unquoted `\` is an escape, single quotes
* are verbatim, and inside double quotes only `\` and `"` are escapes.
*/
function tokenizeLikeJcode(raw: string): string[] {
const parts: string[] = []
let current = ''
let quote: string | null = null
let escaped = false
let started = false
for (const char of raw) {
if (escaped) {
current += char
started = true
escaped = false
} else if (quote) {
if (char === quote) {
quote = null
} else if (char === '\\' && quote === '"') {
escaped = true
} else {
current += char
started = true
}
} else if (char === '\\') {
escaped = true
started = true
} else if (char === "'" || char === '"') {
quote = char
started = true
} else if (/\s/.test(char)) {
if (started) {
parts.push(current)
current = ''
started = false
}
} else {
current += char
started = true
}
}
if (started) {
parts.push(current)
}
return parts
}
describe('jcode managed hook command quoting', () => {
const paths = [
'C:\\Users\\me\\.orca\\agent-hooks\\jcode-hook.cmd',
'C:\\Users\\First Last\\.orca\\agent-hooks\\jcode-hook.cmd',
'/home/me/.orca/agent-hooks/jcode-hook.sh',
'/Users/First Last/.orca/agent-hooks/jcode-hook.sh',
"/Users/o'brien/.orca/agent-hooks/jcode-hook.sh",
'/Users/quote"odd/.orca/agent-hooks/jcode-hook.sh'
]
it.each(paths)('survives jcode\u2019s tokenizer as one argument: %s', (scriptPath) => {
expect(tokenizeLikeJcode(getJcodeManagedCommand(scriptPath))).toEqual([scriptPath])
})
it('is what a bare path fails to do, which is why the quoting exists', () => {
// Regression anchor: the unquoted Windows path loses every separator, so jcode
// execs `C:Usersme.orcaagent-hooksjcode-hook.cmd` and no hook ever fires.
expect(tokenizeLikeJcode('C:\\Users\\me\\.orca\\agent-hooks\\jcode-hook.cmd')).toEqual([
'C:Usersme.orcaagent-hooksjcode-hook.cmd'
])
})
it('still recognizes its own managed entry on either separator', () => {
for (const scriptPath of paths) {
expect(isJcodeManagedCommand(getJcodeManagedCommand(scriptPath))).toBe(true)
}
expect(isJcodeManagedCommand("'/home/me/.config/my-own-hook.sh'")).toBe(false)
})
})