Skip to content

fix(agent): include Claude Code hook fields in hooks stdin payload (#121)#122

Merged
pablof7z merged 1 commit into
masterfrom
fix/issue-121-hooks-claude-code-stdin
May 30, 2026
Merged

fix(agent): include Claude Code hook fields in hooks stdin payload (#121)#122
pablof7z merged 1 commit into
masterfrom
fix/issue-121-hooks-claude-code-stdin

Conversation

@pablof7z

Copy link
Copy Markdown
Collaborator

Summary

Closes #121. Project hooks fired at pre/post-tool boundaries previously emitted only TENEX-specific fields (event, tool, args, result). Claude Code-compatible tools such as proactive-context expect session_id, cwd, transcript_path, and prompt — without them they received malformed input and produced no output.

Changes

  • crates/tenex-agent/src/project_hooks.rs
    • ProjectHooksRunner gains session_id (the conversation id) and prompt (the conversation's root user message) fields; ::new takes both.
    • Both the pre-tool and post-tool stdin payloads now include session_id, cwd (from the runner's working_dir), transcript_path (always null — TENEX stores transcripts in SQLite, not JSONL), and prompt. result remains post-tool only.
    • The two stdin builders moved onto the runner as methods so they read these fields directly.
  • crates/tenex-agent/src/agent_bootstrap/stages.rsload_project_hooks threads session_id and prompt into ProjectHooksRunner::new.
  • crates/tenex-agent/src/agent_bootstrap/mod.rs — the original_task derivation is hoisted above the hook runner construction so a single source of truth feeds both the hook prompt and the AgentBootstrap field. conversation_id supplies session_id.

Validation

  • cargo build -p tenex-agent — clean
  • cargo test -p tenex-agent — 58 + 12 hook tests pass; the hook_receives_parsed_args_on_stdin test now asserts session_id, cwd, transcript_path (null), and prompt.
  • End-to-end: proactive-context awareness --hook PostToolUse returns a non-empty hookSpecificOutput when fed the new payload shape.

🤖 Generated with Claude Code

)

Project hooks fired at pre/post-tool boundaries previously emitted only
TENEX-specific fields (event, tool, args, result). Claude Code-compatible
tools such as proactive-context expect session_id, cwd, transcript_path,
and prompt; without them they received malformed input and produced no
output.

Extend ProjectHooksRunner with session_id (the conversation id) and prompt
(the conversation's root user message, stable across re-engagements), and
emit them — plus cwd from the runner's working_dir and a null
transcript_path (TENEX stores transcripts in SQLite, not JSONL) — in both
the pre-tool and post-tool stdin payloads.

The original_task derivation in agent_bootstrap is hoisted above the hook
runner construction so a single source of truth feeds both the hook prompt
and the AgentBootstrap field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@claude

claude Bot commented May 30, 2026

Copy link
Copy Markdown

Code Review

Overall: Clean, well-scoped fix that correctly addresses the root cause. The minimal footprint and thorough test assertions are good. A few things worth addressing:


CLAUDE.md violations — doc comment length

CLAUDE.md mandates one short line max for comments, and only when the WHY is non-obvious. Several new doc blocks cross that line:

project_hooks.rs — method docs explain WHAT (the signature already says that):

/// Build the `pre-tool` stdin payload. `args_json` is the tool's argument
/// string; it is embedded as parsed JSON when valid, else as a JSON string.
/// The `session_id`, `cwd`, `transcript_path`, and `prompt` fields make the
/// payload Claude Code-compatible; `transcript_path` is always `null`
/// because TENEX persists transcripts in SQLite, not a JSONL file.
fn pre_tool_stdin(...)

The only non-obvious WHY here is the transcript_path: null rationale. The rest describes the code. Suggestion: drop to one line max, e.g. // transcript_path is null — TENEX transcripts live in SQLite, not JSONL. Same applies to post_tool_stdin.

project_hooks.rsprompt field doc is 2 lines:

/// The conversation's root user message, surfaced to hooks as the Claude
/// Code `prompt`. Stable across re-engagements within the conversation.

One line suffices.

Test comment in hook_receives_parsed_args_on_stdin:

// Claude Code-compatible fields must be present so tools like
// proactive-context can read them. `transcript_path` is always null
// because TENEX stores transcripts in SQLite, not a JSONL file.

Two lines, and the transcript_path rationale is already on the assert itself. One line or none.


to_string_lossy() for cwd

"cwd": self.working_dir.to_string_lossy(),

to_string_lossy() silently replaces non-UTF-8 bytes with \u{FFFD}. A hook receiving a mangled cwd would quietly operate on the wrong directory. In practice project dirs are always valid UTF-8, but the lossy coercion hides the failure. Prefer .to_str().expect("working_dir must be UTF-8") or validate once at construction — this is a better invariant to surface loudly than to silently corrupt.


Missing tool_use_id field

Claude Code hook payloads include a tool_use_id field (the request correlation ID for the tool call). Is that intentionally omitted? If proactive-context or other tools ever key off it, silence here will confuse them. Worth documenting the decision or adding a placeholder even if it's null for now.


Test constructor repetition (minor)

The runner constructor call appears 7 times with identical "test-session" / "test prompt" strings. A one-liner helper like fn runner(config, dir) -> ProjectHooksRunner would reduce the noise without any abstraction overhead. Not blocking, but 7 identical blocks is past the "three similar lines" threshold.


What's solid

  • Hoisting original_task above hook construction is the right fix for the delegation-callback bug — single source of truth, no duplication.
  • Moving the free functions to methods rather than adding extra parameters is clean.
  • Test assertions on all four new fields (session_id, cwd, transcript_path, prompt) are exactly what's needed.
  • transcript_path: serde_json::Value::Null is the correct JSON null representation.

🤖 Generated with Claude Code

@pablof7z pablof7z merged commit 5ba7537 into master May 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(hooks): align stdin payload with Claude Code hook protocol

1 participant