A Claude Code PreToolUse hook that prevents background subagents from being killed prematurely in Discord-driven sessions.
When Claude Code runs via a Discord bot (or any system where the session ends when output stops), background subagents get killed immediately. The turn ends as soon as Claude stops emitting output, which sends [Request interrupted by user] to any in-flight background agents.
This affects:
Agenttool calls withrun_in_background: trueTasktool calls withrun_in_background: true- All
Workflowtool calls (which always run detached)
Foreground subagents survive because the turn stays open until they return.
This hook intercepts Task, Agent, and Workflow tool calls in Discord sessions and denies them if they would run in the background. It returns a clear error message telling Claude to re-run synchronously.
- Detection: Checks the session transcript for the Discord envelope marker (
[ANDY-INTERNAL-TASK] [discord-thread]or[discord-mention]) - Filtering: Only polices
Task,Agent, andWorkflowtools — everything else passes through - Decision:
Workflow→ always denied (runs fully detached)Agent/Taskwithrun_in_background: true→ deniedAgent/Taskwithoutrun_in_background→ allowed (foreground)
- Response: Returns a JSON denial with an explanation and workaround
- Save the script to your hooks directory (e.g.,
~/.claude/hooks/discord-foreground-subagent-guard.sh) - Make it executable:
chmod +x discord-foreground-subagent-guard.sh - Add to your
.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Task|Agent|Workflow",
"hooks": [
{
"type": "command",
"command": "/path/to/discord-foreground-subagent-guard.sh"
}
]
}
]
}
}Discord detection: Modify the grep pattern on line 31 if your system uses a different envelope marker.
Parallelization advice: The hook's error message includes a "DISK-PATTERN" tip for fan-out parallelization. Edit or remove this in the DISK_PATTERN variable if it doesn't apply to your use case.
jqfor JSON parsinggrepwith regex support- Bash
MIT — use freely.
#!/usr/bin/env bash
# PreToolUse hook: in Discord-driven sessions, deny backgrounded subagents.
#
# WHY: host-bridge ends the Claude turn as soon as the assistant stops emitting,
# which aborts any in-flight backgrounded child with "[Request interrupted by
# user]". That kills Workflow agents and Task/Agent calls spawned with
# run_in_background:true. Foreground subagents survive because the turn does not
# end until they return. So in Discord sessions we force foreground: deny
# background, deny Workflow, and tell the model to re-run synchronously.
#
# DETECTION: the triggering user message in a Discord turn is injected by
# host-bridge as a string starting "[ANDY-INTERNAL-TASK] [discord-thread]" or
# "[discord-mention]". CLI sessions never carry that envelope. The env-var
# marker CLAUDE_SPAWNED_BY_HOST_BRIDGE does NOT reach this process (claude runs
# as a herdr tab, not a host-bridge child — verified: 0/9 live processes carry
# it), so the transcript envelope is the only reliable signal.
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path // ""')
# Only police subagent-spawning tools; everything else passes untouched.
case "$TOOL_NAME" in
Task|Agent|Workflow) ;;
*) exit 0 ;;
esac
# Not a Discord session → allow. Backgrounding in an interactive CLI session is
# intentional and must not be blocked.
[ -n "$TRANSCRIPT" ] || exit 0
grep -m1 -q '"content":"\[ANDY-INTERNAL-TASK\] \[discord-\(thread\|mention\)\]' "$TRANSCRIPT" || exit 0
# In a Discord session. Decide whether this specific call backgrounds.
BACKGROUND=$(echo "$INPUT" | jq -r '.tool_input.run_in_background // false')
if [ "$TOOL_NAME" = "Workflow" ]; then
REASON="Workflow agents run detached and are killed when the Discord turn ends. Run the work as foreground Task/Agent subagents instead (no run_in_background) so their activity streams to the thread and survives to completion."
elif [ "$BACKGROUND" = "true" ]; then
REASON="run_in_background subagents are killed when the Discord turn ends. Re-run this Task/Agent WITHOUT run_in_background so it runs in the foreground — the turn stays open until it finishes and its progress streams to the thread."
else
# Foreground Task/Agent in a Discord session — exactly what we want. Allow.
exit 0
fi
DISK_PATTERN="DISK-PATTERN FOR FAN-OUT: when spawning MULTIPLE readers/researchers, have EACH subagent WRITE its findings to a file (mkdir -p /tmp/andy-readers; write /tmp/andy-readers/<slug>.md) and RETURN ONLY a short pointer + 1-line summary — never the full content. N pointers cost a few hundred tokens; N full payloads cost tens of thousands and can trip auto-compaction mid-turn, which silently eats your synthesis. When combining, read the files back (or hand the file list to ONE synthesis subagent that reads them in its own context, not yours)."
cat << JSON
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Backgrounded subagents do not survive Discord turns",
"additionalContext": "DISCORD FOREGROUND RULE: $REASON To parallelize, spawn multiple foreground subagents in a SINGLE message — they run concurrently and the turn waits for all of them before ending. $DISK_PATTERN"
}
}
JSON