Skip to content

Instantly share code, notes, and snippets.

@alexknowshtml
Created June 30, 2026 03:13
Show Gist options
  • Select an option

  • Save alexknowshtml/1e771e74227e5f8071011b1dafb53c22 to your computer and use it in GitHub Desktop.

Select an option

Save alexknowshtml/1e771e74227e5f8071011b1dafb53c22 to your computer and use it in GitHub Desktop.
Claude Code PreToolUse hook: prevent background subagents from dying in Discord sessions

discord-foreground-subagent-guard.sh

A Claude Code PreToolUse hook that prevents background subagents from being killed prematurely in Discord-driven sessions.

The Problem

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:

  • Agent tool calls with run_in_background: true
  • Task tool calls with run_in_background: true
  • All Workflow tool calls (which always run detached)

Foreground subagents survive because the turn stays open until they return.

The Solution

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.

How It Works

  1. Detection: Checks the session transcript for the Discord envelope marker ([ANDY-INTERNAL-TASK] [discord-thread] or [discord-mention])
  2. Filtering: Only polices Task, Agent, and Workflow tools — everything else passes through
  3. Decision:
    • Workflow → always denied (runs fully detached)
    • Agent/Task with run_in_background: true → denied
    • Agent/Task without run_in_background → allowed (foreground)
  4. Response: Returns a JSON denial with an explanation and workaround

Installation

  1. Save the script to your hooks directory (e.g., ~/.claude/hooks/discord-foreground-subagent-guard.sh)
  2. Make it executable: chmod +x discord-foreground-subagent-guard.sh
  3. Add to your .claude/settings.json:
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Task|Agent|Workflow",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/discord-foreground-subagent-guard.sh"
          }
        ]
      }
    ]
  }
}

Customization

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.

Requirements

  • jq for JSON parsing
  • grep with regex support
  • Bash

License

MIT — use freely.


The Script

#!/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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment