Skip to content

fix(hooks): pass the tool exit code to Runtime API tool_call_after - #6656

Merged
3 commits merged into
mainfrom
fix/runtime-hook-exit-code
Sep 28, 2026
Merged

3 commits merged into
mainfrom
fix/runtime-hook-exit-code

Conversation

@Hmbown

@Hmbown Hmbown commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

What

Hooks now get a shell command's real exit code, and how it ended, on both the TUI and Runtime API threads. That holds for failing commands as well as passing ones.

There were two causes:

  1. Runtime API path passed no exit code. fire_runtime_tool_completion_hooks (crates/tui/src/runtime_threads.rs) called .with_tool_result(&text, success, None), so DEEPSEEK_TOOL_EXIT_CODE was never set on Runtime API threads.
  2. A failing bash command lost its exit code on both surfaces. finish_contract_bash_result (crates/tui/src/tools/shell.rs) reports a nonzero exit, a timeout, or a kill as Err(ToolError::ExecutionFailed { message }). The exit_code/status metadata it builds went only on the success path, and the hook helper read metadata only from Ok results. So every failing command reached tool_call_after and on_error with no exit code.

Changes

  • ToolError::ExecutionFailed gains metadata: Option<Value> (crates/tools/src/lib.rs), along with ToolError::execution_failed_with_metadata and ToolError::metadata(). execution_failed(..) is unchanged (None). Patterns that named { message } now say { message, .. }. The error's display text and the model-facing contract ("nonzero must be a failed tool call") are unchanged.
  • bash's failure path attaches the same metadata a success carries (exit_code, status, duration_ms, …).
  • A new HookContext::with_tool_outcome(&result) sets the text, success flag, exit code, and status from a settled call. The TUI (tui/tool_routing.rs) and Runtime API (runtime_threads.rs) completion hooks both use it now, so each drops its own copy of the text/success/exit-code derivation. reported_tool_exit_code reads metadata from an Ok result or an Err, and is private to hooks::executor.
  • A new env var, DEEPSEEK_TOOL_STATUS: completed, failed, timed_out, killed, or running. It is set only when a shell tool recorded one of those statuses, and never passes through an arbitrary metadata string. A timed-out command has no exit code, and this is how a hook can tell it apart.
  • docs/HOOKS.md, docs/zh_hans/HOOKS.md, and the CHANGELOG are updated. crates/tui/CHANGELOG.md is synced.
  • The hooks module doc no longer says only the TUI fires hooks. docs/HOOKS.md already listed Runtime API threads.

Tests

Both paths run the real bash tool for exit 0, exit 1, exit 127 (a missing command), and a timeout. They assert what tool_call_after and on_error receive:

case DEEPSEEK_TOOL_EXIT_CODE DEEPSEEK_TOOL_STATUS DEEPSEEK_TOOL_SUCCESS on_error
exit 0 0 completed true no
exit 1 1 failed false yes, same values
exit 127 127 failed false yes, same values
timeout unset timed_out false yes, same values
  • TUI: tui::tool_routing::tests::bash_completion_hooks_get_exit_code_and_status_for_failures (through handle_tool_call_complete).
  • Runtime API: runtime_threads::tests::runtime_shell_completion_delivers_exit_code_and_status_to_hooks (through fire_runtime_tool_completion_hooks).
  • reported_tool_exit_code_reads_only_real_metadata_codes now also covers an error carrying code 127, a timeout with a status but no code, and an unknown status string that is not passed through.
  • The shell tests contract_bash_nonzero_is_an_error_with_status_after_output and lowercase_bash_timeout_uses_seconds_and_fails now assert that the error carries exit_code/status.

Results:

  • The focused codewhale-tui --lib run for the hook, shell, and tool_routing filters: 52 passed, 0 failed. The hooks::, tool_routing::, error_taxonomy, and protocol_parity run: 224 passed, 0 failed. codewhale-tools: 29 passed, 0 failed.
  • Negative control: without the metadata on the error, both new tests and the 2 updated shell tests fail (4 failed). The hooks saw call-exit-1 unset unset false.
  • Gates: cargo fmt --check is clean. Clippy (-p codewhale-tools -p codewhale-tui --all-targets --all-features, CI flags) is clean. These all pass: sync-changelog --check, check-versions --range-audit-advisory, contributor credit, the cheap Lint scripts, the web derive scripts, and vitest lib/public-copy.test.ts (6 passed). The persistence-backlog and runtime-contract measurement budgets were not run locally; CI runs them.

Refs #6582. The structured receipt feature requested there stays open.

🤖 Generated with Claude Code

@Hmbown Hmbown added this to the v0.10.1 milestone Sep 26, 2026
Copilot AI lite review requested due to automatic review settings September 26, 2026 19:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-26T19:04:13.666332Z a1db242 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

CodeWhale Bot and others added 2 commits September 26, 2026 19:33
fire_runtime_tool_completion_hooks hard-coded the exit code to None, so
DEEPSEEK_TOOL_EXIT_CODE was never set and exit_code conditions never
matched on Runtime API threads, while the TUI read it from the tool's
metadata. Move reported_tool_exit_code from tui::tool_routing into
hooks::executor and have both surfaces use it; its unit test moves with
it. Also correct the hooks module doc, which still said only the TUI
fires hooks (docs/HOOKS.md already lists Runtime API threads).

Refs #6582

Tests (focused, codewhale-tui --lib): 22 passed, 0 failed
  (runtime_tool_completion_fires_after_and_error_hooks,
  runtime_shell_completion_delivers_exit_code_to_after_hook,
  reported_tool_exit_code_reads_only_real_metadata_codes, tool_routing::).
Negative control: with the exit code discarded, the new test fails
  ("call-shell unset" vs "call-shell 0").
Gates: cargo fmt --check clean; clippy -p codewhale-tui --all-targets
  --all-features with CI flags clean; sync-changelog --check, provider
  registry, command boundaries, migration manifest, reqwest builders,
  dead-code and blocking-calls budgets, locale parity, product
  vocabulary, contributor credit all pass.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bash reports a nonzero exit, timeout, or kill as
Err(ToolError::ExecutionFailed), and that error carried no metadata; the
exit_code/status metadata was built only for the success path, and the
hook helper read metadata only from Ok results. So every failing command
reached tool_call_after and on_error with no DEEPSEEK_TOOL_EXIT_CODE, in
the TUI and on Runtime API threads.

- ToolError::ExecutionFailed gains `metadata: Option<Value>`, with
  execution_failed_with_metadata() and metadata(). execution_failed() and
  the error's display text are unchanged, so the model still sees a failed
  call with the same message.
- bash's failure path attaches the same metadata a success carries.
- HookContext::with_tool_outcome(&result) sets text, success, exit code
  and status; the TUI and Runtime API completion hooks both use it and
  drop their own copies of that derivation.
- New DEEPSEEK_TOOL_STATUS: completed / failed / timed_out / killed /
  running, only from the shell statuses tools record.
- docs/HOOKS.md (+ zh_hans), CHANGELOG, synced crates/tui/CHANGELOG.md,
  regenerated web/lib/changelog.generated.ts.

Refs #6582

Tests (focused, codewhale-tui --lib): 52 passed, 0 failed for the hook,
  shell and tool_routing filters, including the new
  bash_completion_hooks_get_exit_code_and_status_for_failures (TUI) and
  runtime_shell_completion_delivers_exit_code_and_status_to_hooks
  (Runtime API), each covering exit 0, exit 1, exit 127 and a timeout;
  hooks::/tool_routing::/error_taxonomy/protocol_parity run: 224 passed,
  0 failed; codewhale-tools: 29 passed, 0 failed.
Negative control: without the error metadata those 2 tests plus the 2
  updated shell tests fail (4 failed; hooks saw "call-exit-1 unset unset").
Gates: cargo fmt --check clean; clippy -p codewhale-tools -p codewhale-tui
  --all-targets --all-features with CI flags clean; sync-changelog --check,
  check-versions --range-audit-advisory, contributor credit (v0.10.0 and
  default), provider registry, command boundaries, migration manifest,
  reqwest builders, dead-code and blocking-calls budgets, locale parity,
  product vocabulary, README translations/locales, web derive scripts,
  derive-install --check, vitest lib/public-copy.test.ts (6 passed).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Hmbown
Hmbown force-pushed the fix/runtime-hook-exit-code branch from 87ae150 to e8e0d4e Compare September 27, 2026 02:55
…-code

# Conflicts:
#	CHANGELOG.md
#	crates/tui/CHANGELOG.md
Hmbown pushed a commit that referenced this pull request Sep 27, 2026
# Conflicts:
#	CHANGELOG.md
#	crates/tui/CHANGELOG.md
#	crates/tui/src/runtime_threads/tests.rs
@Hmbown Hmbown closed this pull request by merging all changes into main in 0bfe04e Sep 28, 2026
@Hmbown
Hmbown deleted the fix/runtime-hook-exit-code branch September 28, 2026 08:40
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.

2 participants