# guard-agent-nesting: an agent hook by Scalably

Canonical: https://scalably.io/hooks/guard-agent-nesting
Source: https://github.com/scalably-io/agent-hooks/blob/v1.0.0/hooks/guard-agent-nesting.sh
Runs on: PreToolUse on Agent|Task
Release: v1.0.0 at commit 53dcece
Integrity: sha256 of the script served at https://scalably.io/hooks/guard-agent-nesting.sh is 8a91486a341dd8a9d2737eab10ea07bb110e625a158127325f94aea2247040c7. Verify: curl -s https://scalably.io/hooks/guard-agent-nesting.sh | shasum -a 256
This is the machine-readable representation of the page at the canonical URL. Same facts, denser format.

## Summary

PreToolUse hook: one level of delegation. Subagents are leaves.

## Install

```
/plugin marketplace add scalably-io/agent-skills
/plugin install agent-hooks@scalably-agent-skills
```

## What it does

The problem it solves: a subagent inherits the parent's toolset, including the
tool that spawns subagents. A worker that decides its task will not fit in
context can hand the whole task to a fresh child, which does the same, and the
work never gets done. Cascades several levels deep have been observed, with
every worker waiting on the one below it.

The fix is structural rather than advisory, because advice is exactly what a
worker in that state ignores. The main thread may fan out. A worker may not.

The signal is agent_id, which the agent protocol sets only when the call comes
from inside a subagent:
```text
  absent or empty  the caller is the main thread, allow
  present          the caller is already a subagent, so this call would create
                   a grandchild, deny
```

It denies rather than asking, because the worker must finish the work itself,
now, and there may be no human watching.

Exit: always 0. The decision travels in the JSON.

## The whole script

```bash
#!/usr/bin/env bash
# PreToolUse hook: one level of delegation. Subagents are leaves.
#
# The problem it solves: a subagent inherits the parent's toolset, including the
# tool that spawns subagents. A worker that decides its task will not fit in
# context can hand the whole task to a fresh child, which does the same, and the
# work never gets done. Cascades several levels deep have been observed, with
# every worker waiting on the one below it.
#
# The fix is structural rather than advisory, because advice is exactly what a
# worker in that state ignores. The main thread may fan out. A worker may not.
#
# The signal is agent_id, which the agent protocol sets only when the call comes
# from inside a subagent:
#   absent or empty  the caller is the main thread, allow
#   present          the caller is already a subagent, so this call would create
#                    a grandchild, deny
#
# It denies rather than asking, because the worker must finish the work itself,
# now, and there may be no human watching.
#
# Exit: always 0. The decision travels in the JSON.

input=$(cat)
tool=$(printf '%s' "$input" | jq -r '.tool_name // ""' 2>/dev/null) || exit 0
case "$tool" in
  Agent|Task) ;;
  *) exit 0 ;;
esac

agent_id=$(printf '%s' "$input" | jq -r '.agent_id // ""' 2>/dev/null) || exit 0
[ -n "$agent_id" ] || exit 0

reason='Nested delegation is disabled: you are already a subagent, and subagents cannot spawn their own subagents. This prevents runaway cascades.

Do the work yourself in this turn. If the data is too large to hold at once:
  - Process it in bounded chunks, reading or aggregating one range or file at a time into a local variable or a scratch file, rather than loading all of it into context.
  - Use a script for heavy aggregation instead of reasoning over raw rows in context.
  - If you were handed a slice, complete only that slice and return its result. Whoever spawned you owns merging the slices and any further fan-out.

Return your actual result, or the path to an output file. Not a plan to delegate.'

jq -nc --arg r "$reason" '{
  hookSpecificOutput: {
    hookEventName: "PreToolUse",
    permissionDecision: "deny",
    permissionDecisionReason: $r
  }
}' 2>/dev/null
exit 0
```
