Claude Code's Task tool spawns subagents in the foreground by default, which blocks your input for 10-30+ seconds per call. Telling it to use run_in_background: true in CLAUDE.md works maybe 70% of the time. A PreToolUse hook works every time.
#!/usr/bin/env bash
# PreToolUse hook (Task): silently inject run_in_background: true
# Keeps the lead unblocked so the user can always type
set -euo pipefail
INPUT=$(cat)
# Already background - pass through
echo "$INPUT" | jq -e '.tool_input.run_in_background == true' &>/dev/null && exit 0
# Silently add run_in_background (pass full input - updatedInput may replace, not merge)
echo "$INPUT" | jq '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "allow",
updatedInput: (.tool_input + { run_in_background: true })
}
}'
Register it in settings.json with "matcher": "Task" (full config in this gist).
updatedInput changes the tool parameters before the call runs, and Claude never sees the change. No deny-and-retry, no wasted tokens.
updatedInput vs deny
Most hook examples online deny the call and make Claude retry. That makes sense when the fix needs judgment, like secrets in code or a destructive git command. When the fix is always the same parameter, the retry is just a wasted round trip.
Other conventions you could inject the same way:
isolation: "worktree"on all teammate spawns--draftongh pr createcommandsdangerouslyDisableSandbox: truefor git commits (SSH agent signing)
If a hook would always apply the same fix, use updatedInput. Deny when it needs a decision.
-Tom