Skip to content

perf: PostToolUse hook optimization (~200ms → <30ms)#35

Open
KRRT7 wants to merge 8 commits into
warpdotdev:mainfrom
KRRT7:perf/post-tool-use-optimization
Open

perf: PostToolUse hook optimization (~200ms → <30ms)#35
KRRT7 wants to merge 8 commits into
warpdotdev:mainfrom
KRRT7:perf/post-tool-use-optimization

Conversation

@KRRT7
Copy link
Copy Markdown

@KRRT7 KRRT7 commented May 7, 2026

Summary

End-to-end optimization of the PostToolUse hook, which fires on every tool call. Eliminates redundant process spawns, consolidates jq invocations, and backgrounds the tty write to remove render-pressure spikes.

  • Baseline overhead: 93ms mean per tool call → 26ms mean
  • Spike overhead: 2000-2200ms under render pressure → eliminated (non-blocking write)
  • Cumulative session impact: ~20 tool calls × 93ms = 1.9s → ~20 × 26ms = 0.5s

Benchmark (Apple M3 Pro, macOS 15.4)

Scenario Mean ± σ Min Speedup
Baseline (main) 93.0ms ± 43.3ms 43.8ms
Optimized 26.0ms ± 9.2ms 17.4ms 3.6x
Non-Warp fast exit 12.6ms min 12.6ms 7.4x

Measured with hyperfine --warmup 5 --runs 50.

Changes

# Commit Description
1 perf: add early exit for non-Warp environments Fast-path exit 0 before sourcing anything
2 perf: inline OSC 777 write Eliminate subprocess to warp-notify.sh + redundant gate
3 perf: collapse 4 jq invocations into single pass Single jq -nc reads stdin and builds payload
4 perf: background tty write Prevent PTY congestion blocking
5 fix: validate protocol version, trailing slash, empty payload guard Robustness fixes
6 test: add unit tests for on-post-tool-use.sh 17 new tests (56 total, 80% coverage on target file)

What each change does

  • Early exit: Single env var test before any source or SCRIPT_DIR resolution. Non-Warp environments (subagents, CI) exit in ~1ms.
  • Inline notify: Removes subprocess to warp-notify.sh which re-sourced should-use-structured.sh and re-ran the gate. Saves a bash spawn + redundant function call.
  • Single jq: Replaces 4 jq process spawns (3 in build_payload + 1 for tool_name) with a single jq -nc that reads stdin and constructs the full payload. Protocol negotiation inlined as bash arithmetic.
  • Non-blocking tty: Backgrounds the printf > /dev/tty to prevent synchronous blocking when Warp's PTY is congested during heavy screen rendering.
  • Robustness: Validates non-numeric protocol version (falls back to 1), strips trailing slashes for correct project name extraction, guards against empty payload from jq failure.
  • Tests: 17 new tests exercise payload construction, protocol negotiation, early exits, trailing slash handling, and non-numeric env var fallback. Uses script command to capture /dev/tty output.

Design decisions

  • Inline duplication over shared abstraction: PostToolUse now duplicates some logic from build-payload.sh (protocol negotiation, payload structure). Acceptable because it's the only hot-path hook (~20 calls/session vs 1-2 for others), and the shared files remain for other hooks.
  • Background write safety: Notification is fire-and-forget. If a background write fails, Warp misses one Blocked→Running transition which self-corrects on the next tool call.
  • should_use_structured kept for version gate: The early env var check is a fast-path for non-Warp environments only. The full function call remains to handle broken Warp builds that set the env var but can't render structured notifications.
  • Empty payload guard: If jq fails (malformed stdin, binary input), the script exits cleanly instead of sending a broken OSC sequence to the terminal.
Reproduce the benchmark

Requires hyperfine (brew install hyperfine).

export WARP_CLI_AGENT_PROTOCOL_VERSION=1
export WARP_CLIENT_VERSION="v0.2026.04.29.08.57.preview_01"
INPUT='{"tool_name":"Read","session_id":"bench-session","cwd":"/Users/you/project"}'

# Baseline
git checkout main
hyperfine \
  --warmup 5 \
  --runs 50 \
  --shell=bash \
  -n 'PostToolUse (baseline)' \
  "echo '$INPUT' | bash plugins/warp/scripts/on-post-tool-use.sh"

# Optimized
git checkout perf/post-tool-use-optimization
hyperfine \
  --warmup 5 \
  --runs 50 \
  --shell=bash \
  -n 'PostToolUse (optimized)' \
  "echo '$INPUT' | bash plugins/warp/scripts/on-post-tool-use.sh"

# Non-Warp fast-path (env var unset)
unset WARP_CLI_AGENT_PROTOCOL_VERSION
hyperfine \
  --warmup 5 \
  --runs 50 \
  --shell=bash \
  -n 'PostToolUse (non-Warp fast exit)' \
  "echo '$INPUT' | bash plugins/warp/scripts/on-post-tool-use.sh"

# Verify payload correctness:
export WARP_CLI_AGENT_PROTOCOL_VERSION=1
export WARP_CLIENT_VERSION="v0.2026.04.29.08.57.preview_01"
echo '{"tool_name":"Read","session_id":"bench-session","cwd":"/Users/you/project"}' | \
  bash plugins/warp/scripts/on-post-tool-use.sh
# (check /dev/tty output for OSC 777 sequence with correct JSON payload)

# Run test suite:
bash plugins/warp/tests/test-hooks.sh

Generated by Codeflash Agent

Test plan

  • bash plugins/warp/tests/test-hooks.sh passes (56/56)
  • hyperfine: optimized 26ms mean, 17.4ms min (3.6x faster)
  • hyperfine: non-Warp fast-path 12.6ms min
  • Payload JSON format matches baseline output
  • Trailing slash in cwd produces correct project name
  • Non-numeric protocol version falls back to 1
  • Empty/malformed stdin exits cleanly (no broken OSC)
  • Verify notifications render correctly in Warp

KRRT7 added 7 commits May 7, 2026 12:07
Check WARP_CLI_AGENT_PROTOCOL_VERSION before sourcing any files.
In non-Warp terminals, subagents, and CI this avoids the cost of
SCRIPT_DIR resolution, sourcing should-use-structured.sh, and
calling the function — saving ~10-15ms per tool call in those envs.
Replace the subprocess call to warp-notify.sh with an inline printf.
This eliminates a bash spawn, a second source of should-use-structured.sh,
and a redundant should_use_structured() call — saving ~20-30ms per
PostToolUse invocation.
Replace sourcing build-payload.sh (which spawns jq 3 times for
session_id, cwd, and payload construction) plus the tool_name
extraction (1 more jq) with a single jq -nc call that reads stdin
and builds the entire payload in one process. Saves ~60ms per
PostToolUse invocation (3 fewer process spawns at ~20ms each).

Protocol version negotiation is inlined as a simple bash comparison
since the plugin version is a constant (1).
The printf to /dev/tty blocks synchronously when Warp's PTY is
congested during heavy screen rendering, causing 2000-2200ms spikes.
Backgrounding the write makes the hook return immediately regardless
of terminal render state. The notification is fire-and-forget — no
response is expected.
- Guard against non-numeric WARP_CLI_AGENT_PROTOCOL_VERSION with
  regex check — prevents jq --argjson crash on garbage input.
- Use jq sub(".*/"; "") instead of split/last — correctly returns ""
  for root "/" rather than split artifact.
- Remove redundant comments; the commit history explains the "why".
Add 16 tests covering:
- Full payload construction (all fields)
- Missing/empty fields produce empty strings
- Protocol version negotiation (capped at 1, non-numeric fallback)
- Early exit paths (no env var, broken Warp version)

Uses `script` command to capture /dev/tty output from the real hook
script, then extracts and validates the JSON payload.

Coverage for on-post-tool-use.sh: 6.7% → 80.0%
Script fixes:
- Strip trailing slashes before extracting project name via
  sub("/+$"; "") — matches basename behavior for paths like
  "/Users/alice/project/".
- Add 2>/dev/null on jq call to suppress stderr noise.
- Guard against empty BODY (jq failure on malformed stdin) —
  exit before sending broken OSC sequence.

Test fixes:
- Use temp file for input instead of echo interpolation (prevents
  shell injection if input contains single quotes).
- Add `wait` before sleep to ensure background printf completes.
- Increase sleep to 0.2s for CI robustness.
- Add test for trailing slash in cwd.
@alokedesai alokedesai requested a review from harryalbert May 7, 2026 18:34
@KRRT7
Copy link
Copy Markdown
Author

KRRT7 commented May 7, 2026

Screen.Recording.2026-05-07.at.2.38.01.PM.mov

Copy link
Copy Markdown
Contributor

@harryalbert harryalbert left a comment

Choose a reason for hiding this comment

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

Screen.Recording.2026-05-07.at.2.38.01.PM.mov

hmm, the demo looks a little off. It looks like the done notifications flickers on and then goes away, and then I'm not seeing the claude status change to completed (the little check in the vertical tabs status bar) when it's completed. Am I missing something/are these pre-existing issues?

@KRRT7
Copy link
Copy Markdown
Author

KRRT7 commented May 7, 2026

I just ran it from the main version and I see flickering still

@harryalbert
Copy link
Copy Markdown
Contributor

I just ran it from the main version and I see flickering still

Hmm, I'm not seeing flickering when I run using the global plugin

Screen.Recording.2026-05-07.at.4.04.01.PM.mov

@KRRT7
Copy link
Copy Markdown
Author

KRRT7 commented May 7, 2026

I think I know why it's happening, let me debug rq

skspade added a commit to skspade/claude-code-warp that referenced this pull request May 13, 2026
skspade added a commit to skspade/claude-code-warp that referenced this pull request May 13, 2026
Resolution: took pr-27's version of on-post-tool-use.sh and test-hooks.sh.
This sacrifices warpdotdev#35's PostToolUse-specific perf inline-jq optimization
(which used a direct /dev/tty write, bypassing warpdotdev#44's tty walker) in
favor of using the patched warp-notify.sh helper, plus gains
tool_preview and permission_mode fields.
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