repl: pre/post CMD hooks via config.hooks (closes #3)
Optional shell scripts trigger around every CMD: execution. Use cases:
audit logging, auto-format-after-edit, custom safety gates beyond the
existing confirm_cmd boolean.
Config shape:
hooks = {
pre_cmd = "/path/to/pre-script",
post_cmd = "/path/to/post-script",
}
Contract per hook invocation:
- The command line is piped to the hook on stdin.
- Env vars: AISH_CMD (the command), AISH_TURN (#ctx.turns at the
moment of dispatch), AISH_CWD (libc.getcwd() result).
- Hook stdout is streamed live to the terminal via executor.exec
(so the user sees its output regardless of exit status).
Pre-hook: non-zero exit aborts the command and emits a status line
including the exit code. last_exec_code is set to the hook's exit
so the {last_status} prompt template variable reflects the abort.
Post-hook: exit code is ignored (the spec says so); only the visible
stdout matters. Runs after the command's exec_end frame.
Tested with success, abort, and stdin-matches-env paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+10
@@ -61,6 +61,16 @@ return {
|
||||
dir = (os.getenv("HOME") or ".") .. "/.local/share/aish",
|
||||
},
|
||||
|
||||
-- Issue #3: pre/post CMD hooks. Optional shell scripts triggered around
|
||||
-- every CMD: execution. Each hook receives the command on stdin and
|
||||
-- AISH_CMD / AISH_TURN / AISH_CWD as env vars. Non-zero exit on pre_cmd
|
||||
-- aborts execution; post_cmd's exit code is ignored but its stdout is
|
||||
-- logged. Default off (no hooks). Uncomment to enable.
|
||||
-- hooks = {
|
||||
-- pre_cmd = (os.getenv("HOME") or ".") .. "/.aish/hooks/pre-cmd",
|
||||
-- post_cmd = (os.getenv("HOME") or ".") .. "/.aish/hooks/post-cmd",
|
||||
-- },
|
||||
|
||||
-- Phase 2 (docs/PHASE2.md): MCP server registry + tool-call policy.
|
||||
-- The block is OFF by default — connect-at-startup happens only when
|
||||
-- `servers` is non-empty. Uncomment + adjust per your fleet.
|
||||
|
||||
@@ -564,6 +564,27 @@ function M.run(config)
|
||||
-- a [exec output] block pending until ask_ai flushes it via append_user.
|
||||
-- Direct user-role injection violated chat-template alternation (mistral-
|
||||
-- nemo's Jinja rejects user/user back-to-back); see PHASE0.md §6.
|
||||
--
|
||||
-- Issue #3: pre_cmd / post_cmd hooks fire around exec. Each hook
|
||||
-- receives the command on stdin and AISH_CMD/AISH_TURN/AISH_CWD as
|
||||
-- env vars. Non-zero exit on pre_cmd aborts. post_cmd exit is
|
||||
-- ignored; its stdout is logged via renderer.status.
|
||||
local function _shq(s) return "'" .. (s or ""):gsub("'", [['\'']]) .. "'" end
|
||||
local function _run_hook(script, cmd, want_output)
|
||||
local cwd = (require("ffi.libc").getcwd()) or os.getenv("PWD") or "?"
|
||||
local pipeline = string.format(
|
||||
"printf '%%s' %s | AISH_CMD=%s AISH_TURN=%d AISH_CWD=%s %s 2>&1",
|
||||
_shq(cmd), _shq(cmd), #ctx.turns, _shq(cwd), _shq(script))
|
||||
if want_output then
|
||||
local out, code = executor.exec(pipeline)
|
||||
return code, out
|
||||
else
|
||||
local out, code = executor.exec(pipeline)
|
||||
-- Even when we don't *want* output, surface it if the hook
|
||||
-- aborts so the user sees why.
|
||||
return code, out
|
||||
end
|
||||
end
|
||||
local function run_shell(cmd)
|
||||
local chd, err = executor.maybe_chdir(cmd)
|
||||
if chd ~= nil then
|
||||
@@ -575,6 +596,16 @@ function M.run(config)
|
||||
end
|
||||
return
|
||||
end
|
||||
local hooks = config.hooks or {}
|
||||
if hooks.pre_cmd then
|
||||
local rc = _run_hook(hooks.pre_cmd, cmd, false)
|
||||
if rc ~= 0 then
|
||||
renderer.status(("pre_cmd hook aborted (exit %d): %s")
|
||||
:format(rc, cmd))
|
||||
last_exec_code = rc
|
||||
return
|
||||
end
|
||||
end
|
||||
renderer.exec_begin()
|
||||
local out, code = executor.exec(cmd)
|
||||
last_exec_code = code
|
||||
@@ -582,6 +613,9 @@ function M.run(config)
|
||||
if config.shell and config.shell.capture_output then
|
||||
ctx:append_exec_output(out)
|
||||
end
|
||||
if hooks.post_cmd then
|
||||
_run_hook(hooks.post_cmd, cmd, true)
|
||||
end
|
||||
end
|
||||
|
||||
-- Send user text to the active model and process the response. If MCP
|
||||
|
||||
Reference in New Issue
Block a user