feat(agent): add pre-execute hook + move turn reminders to user message#124
Conversation
…to user message Adds `HookEvent::PreExecute` to the hooks system: fires once per turn before the LLM call, cannot block, and injects stdout into the user message as `<system-reminder type="pre-execute-hook">` blocks. Moves the active-tools, active-shell-tasks, and file-modifications `<system-reminder>` blocks from the system prompt to the user message so the system prompt stays byte-stable across re-engagements and maintains prompt-cache hits. https://claude.ai/code/session_01PHKUKH8Ejfq3yeE8DuwJob
Code Review
OverviewTwo well-scoped changes:
The core fix is solid and the new feature follows existing patterns cleanly. Code QualityStrengths:
Issues1. Stderr silently discarded on non-zero pre-execute exit HookRun::Completed { exit_ok, stdout, stderr: _ } => {
if !exit_ok {
eprintln!("[tenex-agent] warn: pre-execute hook '{}' exited non-zero ...", hook.name);
}
}The warning tells the operator that the hook failed but not why. If HookRun::Completed { exit_ok, stdout, stderr } => {
if !exit_ok {
eprintln!("[tenex-agent] warn: pre-execute hook '{}' exited non-zero{}: pre-execute hooks cannot block",
hook.name,
if stderr.is_empty() { String::new() } else { format!(": {stderr}") }
);
}
}This is a minor DX issue — non-zero exit is already semantically ignored, so there's no correctness concern. 2. let pre_execute_hooks = project_hooks.clone();This is a full clone of all hooks, not a filtered set. The name implies it only contains 3. Missing negative-path tests for The tests cover the happy path and payload shape well, but there are no tests for:
Given the explicit "cannot block" contract, these paths deserve test coverage. PerformanceNo concerns. The SecurityNo new attack surface. Hook commands are operator-controlled via Summary
Clean implementation overall. The three issues above are all minor — none would justify a revert or a follow-up patch on their own, but the stderr/naming points are worth keeping in mind for the next hooks-related change. Reviewed by Claude Code |
Summary
active-tools,active-shell-tasks, andfile-modifications<system-reminder>blocks were appended tosystem_promptviapush_str, mutating it per invocation and breaking prompt-cache hits. They now go into the user message instead, keepingsystem_promptbyte-stable across re-engagements.pre-executehook event type — fires once per user-message turn before the LLM call. Cannot block; non-zero exit/timeout/spawn failures are warned and ignored. Stdout is injected into the user message as<system-reminder type="pre-execute-hook">.Changes
project_hooks.rsHookEvent::PreExecutewith wire name"pre-execute"fire_pre_execute()method andpre_execute_stdin()payload builder (notool/argsfields)"pre-execute"eventsrun_hook(hook ignores stdin and exits early)agent_bootstrap/helpers.rscompose_user_messagewith areminders: &[String]parameteragent_bootstrap/mod.rssystem_promptis now immutable (letnotlet mut)turn_reminders: Vec<String>and pre-execute hook injections appended therecompose_user_messagecall at the end passes all reminders into the user messageAGENTS.md— documents the three hook event types and their blocking semanticsTest plan
cargo test -p tenex-agentpasses (153 tests, 0 failures)<system-reminder type="pre-execute-hook">in the user turnGenerated by Claude Code