[BUG] Claude Code — Per-Agent Permission Control Gap

Resolved 💬 7 comments Opened Apr 30, 2026 by divakaran5005 Closed May 1, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Problem Summary
Claude Code currently has no mechanism to restrict the main agent from writing to specific paths while allowing subagents to write to those same paths within the same session.
This is a critical gap for teams running specialized multi-agent architectures.
The real-world scenario:
I have built a fully specialized subagent system:

gas-script-coder → writes Google Apps Script code
gas-code-review → reviews code quality
gas-testing-agent → writes and runs tests
gas-error-handler → handles error logging
gas-sheets-builder → builds sheet structures

There is a massive difference in output quality between the main agent directly editing code versus a specialized subagent doing it. The specialized subagent has a focused system prompt, domain expertise, and restricted context — it produces significantly better output for its specific task.
What I need:

Main agent = coordinator only → no write access to src/
Each subagent = specialist → write access ONLY to its assigned files/folders
Example: gas-script-coder can read everything but write to src/scripts/ but NOT src/sheets/
Example: gas-code-review can read everything but write NOWHERE
No subagent can access another subagent's assigned files

Why this matters:
This level of control gives two things: safety and quality. Safety because no agent can accidentally destroy another agent's work. Quality because each agent is forced to stay in its lane and do only what it is specialized for.
What actually happens today:
When I remove write access from the main agent to force delegation to subagents — the subagents also lose write access. They are cut off equally. The main agent ignores subagents and writes directly anyway. The specialized agents become useless. I am forced to either give the main agent full dangerous access OR break the entire pipeline.
This makes the core value of specialized subagents — better output, safer execution, controlled file ownership — completely unreachable.

What Should Happen?

Current Architecture (What We Built)
Goal:

Main agent = coordinator only → writes ONLY to .IDE_Plans/
Subagents = specialists → write to src/, logs/, tests/

Subagents designed:

gas-script-coder → writes GAS scripts to src/
gas-sheets-builder → builds sheet structures in src/
gas-code-review → reviews code in src/
gas-error-handler → handles errors in logs/
gas-testing-agent → writes tests to tests/

Why this architecture makes sense:

Main agent has full project context but should only coordinate
Subagents are specialists — better quality output for focused tasks
If main agent writes code directly, it bypasses specialization
Parallel subagents = faster execution
Isolated context per subagent = cleaner results

=> I have already tried the hooks and Json nothing working.

Error Messages/Logs

What We Tried & Why Each Failed
Attempt 1 — Global deny in settings.json:
json{
  "permissions": {
    "deny": ["Write", "Edit", "Bash"]
  }
}
Result: Blocked main agent AND all subagents equally. No agent could write anything. Project completely broken.

Attempt 2 — Path-specific deny in settings.json:
json{
  "permissions": {
    "deny": ["Write(src/**)","Edit(src/**)"]
  }
}
Result: Blocked subagents too. Project-level deny overrides subagent tools: declarations. All 5 subagents failed — gas-script-coder, gas-sheets-builder, gas-code-review, git-agent, cmd-skills-hooks-builder.

Attempt 3 — Subagent tools: field in .md file:
yaml---
name: gas-script-coder
tools: Read, Write, Glob
---
Result: tools: field is overridden by project-level settings.json deny. Subagent declared Write but still could not write. Confirmed by testing all 5 subagents.

Attempt 4 — Separate settings.json per subagent folder:
agents/gas-script-coder/.claude/settings.json → allow Write
Result: These folder-level settings apply only to standalone Claude Code processes — NOT to subagents invoked via Agent() tool inside the main session. Completely ignored.

Attempt 5 — PreToolUse hooks:
Tried to intercept tool calls and check caller identity.
Result: Hook cannot identify whether the caller is main agent or subagent. No caller identity is exposed in hook context. Cannot differentiate.

Root Cause
<br>
Claude Code's permission system is session-wide, not agent-wide.
When a subagent is invoked via Agent() tool:

It loads its .md definition from .claude/agents/
It runs inside the SAME session
It INHERITS the parent session's permission rules
Project-level deny rules CANNOT be overridden by subagent tools: declarations
There is NO way to say "deny main agent only, allow subagent only" for the same path
---
---
# Hook System — src-write-guard Investigation Report

**Last Updated:** 2026-04-30
**Status:** 🔴 ALL APPROACHES FAILED — Debug logging needed to find actual PreToolUse stdin structure

---

## CORE OBJECTIVE

| Actor | src/ Access |
|-------|------------|
| Main agent (the AI talking to you) | BLOCKED — cannot write to src/ directly |
| Any sub-agent (gas-script-coder, gas-sheets-builder, git-flow-expert, etc.) | ALLOWED — can write to src/ |

**Rule:** Main agent must delegate all src/ writes to sub-agents. Cannot bypass via retry or Bash.

---

## SUMMARY — All Approaches (Quick View)

| | Approach 1 | Approach 2 | Approach 3 | Approach 4 |
|---|---|---|---|---|
| Recorder event | `UserPromptSubmit` | `SubagentStart/Stop` | `SubagentStart/Stop` + `SessionEnd` | NONE — no state file |
| Guard event | `Edit\|Write` | `Edit\|Write` | `Edit\|Write` + `Bash` | `Edit\|Write\|Bash` |
| State file logic | exact type match | exact type match | empty = block, non-empty = allow | **NO STATE FILE — tried agent_type from stdin** |
| Bash bypass fixed? | No | No | YES | YES |
| Stale state fixed? | No | No | YES | YES |
| Works for approved agents? | No | No | YES | NO — tested, FAILED |
| **Status** | 🔴 FAILED | 🔴 FAILED | 🟡 READY (not tested) | 🔴 **TESTED — FAILED** |

### Why Each Failed

- **Approach 1:** UserPromptSubmit fires for user messages, not agent messages — state always says "main-agent"
- **Approach 2:** SubagentStop fires BEFORE sub-agent completes its actual write (after user confirmation) — state cleared too early, even approved agents blocked
- **Approach 3:** Uses state file approach with improved logic — still needs testing but more complex than Approach 4

### Approach 4 — TESTED AND FAILED

**Web research claimed:**
- PreToolUse stdin includes `agent_type` for sub-agents
- PreToolUse stdin does NOT have `agent_type` for main agent (field is absent)

**Actual test result: 🔴 FAILED**

**TEST RESULT: 🔴 FAILED — agent_type is NOT present in PreToolUse stdin for sub-agents**

### Lessons from Approach 4 Failure

1. Web research was incorrect — `agent_type` does NOT appear in PreToolUse stdin for sub-agents
2. The PreToolUse JSON structure must be different from what was researched
3. Need to investigate actual PreToolUse stdin structure with debugging

### Next Steps Needed

1. Add debug logging to capture actual PreToolUse stdin structure
2. Find which field(s) actually distinguish main agent from sub-agents
3. Design Approach 5 based on actual observed behavior

---

## FILES TO CHANGE FOR APPROACH 4 (Quick Reference)

| File | Change |
|------|--------|
| `D:\...\src-write-guard.sh` | Replace with APPROACH 4 CANDIDATE script (read agent_type directly from stdin — no state file) |
| `D:\...\settings.json` | Add `src-write-guard.sh` to Bash block + REMOVE `src-agent-recorder.sh` from SubagentStart/SubagentStop (no longer needed) |
| `D:\...\src-agent-recorder.sh` | No longer needed for this purpose — can delete or repurpose |

---

## ONBOARDING FOR NEXT SESSION

If you are continuing this work in a new session, **start here**:

1. Read this report: `.IDE_Plans/hook-src-write-guard-investigation-report.md`
2. Check current state of `settings.json` and `src-write-guard.sh`
3. **Goal:** Main agent blocked from src/, all sub-agents allowed
4. **Do NOT repeat Approach 1** — proven wrong
5. **Do NOT repeat Approach 2** — proven broken
6. **Do NOT repeat Approach 3** — more complex than needed, Approach 4 is simpler but FAILED
7. **Do NOT rely on web research for PreToolUse stdin fields** — research was incorrect
8. **NEXT STEP:** Add debug logging to capture actual PreToolUse stdin structure and find which field distinguishes main vs sub-agent
9. **Design Approach 5** based on actual observed behavior

---

## REMAINING OPEN QUESTIONS

| # | Question | Why it matters | Status |
|---|----------|---------------|--------|
| Q1 | Does PreToolUse stdin have `agent_type`? | **ANSWERED: NO** — Web research was incorrect | 🔴 CONFIRMED ABSENT |
| Q2 | What field(s) in PreToolUse distinguish main vs sub-agent? | **UNKNOWN** — Need debug logging | 🟡 PENDING INVESTIGATION |
| Q3 | Can we identify sub-agents via some other field? | Possibly via `tool`, `prompt`, or other fields | 🟡 PENDING INVESTIGATION |
| Q4 | Debug approach needed | Need to capture actual PreToolUse stdin | 🟡 PENDING — add debug logging |

---

## HISTORY

| Date/Time | Event | Outcome |
|-----------|-------|---------|
| 2026-04-27 AM | Approach 1 (UserPromptSubmit) attempted | 🔴 FAILED — wrong event type |
| 2026-04-27 PM | Approach 2 (SubagentStart/Stop) applied | 🔴 FAILED — SubagentStop clears state before sub-agent writes |
| 2026-04-27 PM | gas-script-coder delegated and blocked | Confirmed approach 2 is fully broken |
| 2026-04-27 PM | Objective changed — allow all sub-agents | New approach needed |
| 2026-04-27 PM | Web research: PreToolUse stdin structure | ✅ FOUND: agent_type is in stdin for sub-agents, absent for main agent |
| 2026-04-27 PM | Approach 4 (direct stdin read) designed | 🟢 SIMPLEST — no state file needed |
| 2026-04-30 | Approach 4 tested | 🔴 FAILED — agent_type NOT in PreToolUse stdin (web research was incorrect) |

---

---

## DETAILED APPROACHES

---

## APPROACH 1 — UserPromptSubmit Recorder 🔴 FAILED

**Intent:** When user types message, record agent_type to state file. PreToolUse reads state file to know who is calling.

**Files changed:** settings.json (UserPromptSubmit hook), src-agent-recorder.sh

**Result:** FAILED — Completely broken

### Why It Failed
- `UserPromptSubmit` fires when USER types a message — not when an agent runs
- The stdin for UserPromptSubmit does NOT contain the running agent's type
- `agent_type` was always null or "main-agent" even when a sub-agent like `gas-script-coder` was running
- State file always said "main-agent" regardless of which agent actually called Write/Edit
- ALL agents (including approved ones) got blocked

### Exact Error Observed

src-write-guard.sh: [HOOK BLOCKED] Only gas-script-coder and gas-script-coder agents can write to src/ folder. Your agent: 'main-agent' is not authorized.


**Lesson learned:** UserPromptSubmit is for user input events, NOT agent execution events. Never use it to track which agent is running.

---

## APPROACH 2 — SubagentStart/SubagentStop Recorder 🔴 FAILED

**Intent:** When a sub-agent starts, record its type to state file. When it stops, clear state. PreToolUse reads state to know who is calling.

**Files changed:** settings.json (SubagentStart/SubagentStop hooks), src-agent-recorder.sh (updated)

**Result:** FAILED — Completely broken — even approved agents cannot write

### What Happened

1. User asked main agent to create src/02_Triggers/readme.md
2. Main agent was blocked (as expected)

3. User delegated to gas-script-coder sub-agent
4. gas-script-coder tried to write src/02_Triggers/readme.md
5. gas-script-coder was ALSO BLOCKED ← NEW FINDING


### The Timing Issue (Root Cause)

Main agent spawns gas-script-coder
  → SubagentStart fires → state = "gas-script-coder" ✓

gas-script-coder asks user: "Should I create readme.md?"
  → User says "yes"

  → SubagentStop fires at this point (gas-script-coder finished its "turn")
  → state file cleared ✗

gas-script-coder actually writes the file
  → PreToolUse fires → reads state file → EMPTY → BLOCKED ✗


**SubagentStop fires BEFORE the sub-agent completes its actual work** — because asking for confirmation is treated as the end of that turn.

**Lesson learned:** SubagentStop fires when the sub-agent asks for confirmation, NOT when it completes its task after confirmation.

---

## APPROACH 3 — New Logic: Empty State = Block, Non-Empty = Allow 🟡 READY FOR TESTING (BUT APPROACH 4 IS SIMPLER)

**Intent:** Remove exact-type checking. Use simple logic: state file has content = sub-agent running = ALLOW. State file is empty = main agent = BLOCK.

**Files to change:** settings.json, src-write-guard.sh

**Result:** NOT YET TESTED — More complex than Approach 4

### What Needs to Change

1. **Add `src-write-guard.sh` to Bash block** — closes the retry bypass gap
2. **Add `SessionEnd` hook** — clears state file so no stale state across sessions
3. **Simplify `src-write-guard.sh` logic** — empty state = block, non-empty = allow

### Why Approach 3 is now OBSOLETE

Approach 4 (below) achieves the same goal without ANY state file — just read `agent_type` directly from PreToolUse stdin. Simpler, fewer failure points.

---

## APPROACH 4 — Direct agent_type Read from PreToolUse stdin 🔴 TESTED — FAILED

**Intent:** No state file needed. PreToolUse stdin already has `agent_type` for sub-agents but NOT for main agent. Use this directly to distinguish main agent from sub-agents.

**Files to change:** settings.json, src-write-guard.sh

**Result:** 🔴 TESTED AND FAILED — `agent_type` does NOT exist in PreToolUse stdin for any agent type

### What Was Tested

1. Deployed Approach 4 script to `.claude/hooks/enforcement/src-write-guard.sh`
2. Tested with gas-script-coder sub-agent
3. **Sub-agent was BLOCKED** — `agent_type` field was absent (empty) in PreToolUse stdin

### Why It Failed

Web research was incorrect. The `agent_type` field does NOT appear in PreToolUse stdin for any agent type (main or sub-agent). The actual PreToolUse JSON structure is different from what was documented online.

### How It Works


PreToolUse fires (for Write, Edit, or Bash to src/)
  → src-write-guard.sh reads stdin JSON
  → Check if agent_type field is present
  → If ABSENT → main agent → BLOCK
  → If PRESENT → sub-agent → check if in approved list → ALLOW or BLOCK


### Why This Works

From Claude Code hooks documentation (verified via web search):
- Sub-agents: PreToolUse stdin includes `"agent_type": "gas-script-coder"` (or whatever type)
- Main agent: PreToolUse stdin does NOT have agent_type field at all (absent, not "main-agent")
- So: `agent_type` absent = main agent = BLOCK, `agent_type` present = sub-agent = check approval

### Behavior Matrix

| Agent | `agent_type` in PreToolUse stdin | Action |
|-------|--------------------------------|--------|
| Main agent | **ABSENT** | BLOCK |
| gas-script-coder | YES — "gas-script-coder" | ALLOW |
| gas-sheets-builder | YES — "gas-sheets-builder" | ALLOW |
| git-flow-expert | YES — "git-flow-expert" | BLOCK (not in approved list) |
| gas-error-handler | YES — "gas-error-handler" | BLOCK (not in approved list) |

---

### CODE CANDIDATE — Approach 4 (BEST)

**File: src-write-guard.sh (APPROACH 4 — NO STATE FILE NEEDED)**

#!/bin/bash
# src-write-guard.sh
# PreToolUse hook: restricts src/ folder writes to only approved sub-agents
#
# Logic (NO state file needed):
#   - agent_type IN stdin -> sub-agent is running -> use it directly
#   - agent_type NOT in stdin -> main agent -> BLOCK
#   - Only gas-script-coder and gas-sheets-builder are allowed
#
# Runs on: Edit, Write, AND Bash (catches retry bypass)

INPUT=$(cat)
if [ -z "$INPUT" ]; then
    exit 0
fi

# Extract file_path from PreToolUse JSON
TOOL_INPUT_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [ -z "$TOOL_INPUT_PATH" ] || [ "$TOOL_INPUT_PATH" = "empty" ] || [ "$TOOL_INPUT_PATH" = "null" ]; then
    exit 0
fi

# Normalize path: convert Windows backslash to forward slash
NORMALIZED_PATH=$(echo "$TOOL_INPUT_PATH" | sed 's/\\/\//g')

# Check if path contains /src/
if echo "$NORMALIZED_PATH" | grep -q "/src/"; then
    # Extract agent_type from stdin — present ONLY for sub-agents
    AGENT_TYPE=$(echo "$INPUT" | jq -r '.agent_type // empty')

    if [ -z "$AGENT_TYPE" ] || [ "$AGENT_TYPE" = "empty" ] || [ "$AGENT_TYPE" = "null" ]; then
        # No agent_type -> main agent (not a sub-agent) -> BLOCK
        echo "[HOOK BLOCKED] Main agent cannot write to src/ folder." >&2
        echo "Only gas-script-coder and gas-sheets-builder sub-agents are allowed." >&2
        exit 2
    fi

    # agent_type is present — check if approved
    if [ "$AGENT_TYPE" = "gas-sheets-builder" ] || [ "$AGENT_TYPE" = "gas-script-coder" ]; then
        exit 0
    else
        echo "[HOOK BLOCKED] Only gas-script-coder and gas-sheets-builder agents can write to src/ folder." >&2
        echo "Your agent: '$AGENT_TYPE' is not authorized." >&2
        exit 2
    fi
fi

exit 0


**File: settings.json (APPROACH 4 CANDIDATE)**

"UserPromptSubmit": [],
"PreToolUse": [
  {
    "matcher": "Edit|Write",
    "hooks": [
      { "command": "bash \".../gs-file-restriction-enforcer.sh\"" },
      { "command": "bash \".../unit-test-checker.sh\"" },
      { "command": "bash \".../plan-validator.sh\"" },
      { "command": "bash \".../src-write-guard.sh\"" }
    ]
  },
  {
    "matcher": "Agent|Bash|Write|Edit|NotebookEdit|Delete|Move|Copy",
    "hooks": [
      { "command": "bash \".../subagent-delegation-enforcer.sh\"" },
      { "command": "bash \".../global-rules-enforcer.sh\"" },
      { "command": "bash \".../src-write-guard.sh\"" }
      // src-write-guard now runs for Bash too — closes retry bypass
    ]
  }
],
// REMOVE src-agent-recorder.sh from SubagentStart/SubagentStop (no longer needed)
// NO SessionEnd hook needed (no state file to clear)


### What to Remove from settings.json

Remove these from SubagentStart and SubagentStop (no longer needed):

- src-agent-recorder.sh
- src-agent-recorder.sh --clear


---

## SUMMARY TABLE — All Approaches

| | Approach 1 | Approach 2 | Approach 3 | Approach 4 |
|---|---|---|---|---|
| Uses state file | Yes | Yes | Yes | **NO** |
| Needs SubagentStart/Stop | Yes | Yes | Yes | **NO** |
| Reads agent_type from stdin | No | No | No | **YES (but field doesn't exist)** |
| Bash bypass fixed | No | No | Yes | **YES** |
| Stale state issue | Yes | Yes | Yes (SessionEnd fixes) | **NO** |
| Complexity | Medium | High | High | **LOW** |
| Status | 🔴 FAILED | 🔴 FAILED | 🟡 Not tested | 🔴 **TESTED FAILED** |

---

## FILES TO CHANGE FOR APPROACH 4

| File | Action |
|------|--------|
| `src-write-guard.sh` | Replace with Approach 4 candidate script above |
| `settings.json` | 1. Add `src-write-guard.sh` to `Agent\|Bash\|Write\|Edit\|...` block |
| `settings.json` | 2. Remove `src-agent-recorder.sh` from SubagentStart |
| `settings.json` | 3. Remove `src-agent-recorder.sh --clear` from SubagentStop |
| `src-agent-recorder.sh` | Delete or repurpose (no longer needed for this hook) |

Steps to Reproduce

Hook System — src-write-guard Investigation Report

Last Updated: 2026-04-30
Status: 🔴 ALL APPROACHES FAILED — Debug logging needed to find actual PreToolUse stdin structure

---

CORE OBJECTIVE

| Actor | src/ Access |
|-------|------------|
| Main agent (the AI talking to you) | BLOCKED — cannot write to src/ directly |
| Any sub-agent (gas-script-coder, gas-sheets-builder, git-flow-expert, etc.) | ALLOWED — can write to src/ |

Rule: Main agent must delegate all src/ writes to sub-agents. Cannot bypass via retry or Bash.

---

SUMMARY — All Approaches (Quick View)

| | Approach 1 | Approach 2 | Approach 3 | Approach 4 |
|---|---|---|---|---|
| Recorder event | UserPromptSubmit | SubagentStart/Stop | SubagentStart/Stop + SessionEnd | NONE — no state file |
| Guard event | Edit\|Write | Edit\|Write | Edit\|Write + Bash | Edit\|Write\|Bash |
| State file logic | exact type match | exact type match | empty = block, non-empty = allow | NO STATE FILE — tried agent_type from stdin |
| Bash bypass fixed? | No | No | YES | YES |
| Stale state fixed? | No | No | YES | YES |
| Works for approved agents? | No | No | YES | NO — tested, FAILED |
| Status | 🔴 FAILED | 🔴 FAILED | 🟡 READY (not tested) | 🔴 TESTED — FAILED |

Why Each Failed

  • Approach 1: UserPromptSubmit fires for user messages, not agent messages — state always says "main-agent"
  • Approach 2: SubagentStop fires BEFORE sub-agent completes its actual write (after user confirmation) — state cleared too early, even approved agents blocked
  • Approach 3: Uses state file approach with improved logic — still needs testing but more complex than Approach 4

Approach 4 — TESTED AND FAILED

Web research claimed:

  • PreToolUse stdin includes agent_type for sub-agents
  • PreToolUse stdin does NOT have agent_type for main agent (field is absent)

Actual test result: 🔴 FAILED

TEST RESULT: 🔴 FAILED — agent_type is NOT present in PreToolUse stdin for sub-agents

Lessons from Approach 4 Failure

  1. Web research was incorrect — agent_type does NOT appear in PreToolUse stdin for sub-agents
  2. The PreToolUse JSON structure must be different from what was researched
  3. Need to investigate actual PreToolUse stdin structure with debugging

Next Steps Needed

  1. Add debug logging to capture actual PreToolUse stdin structure
  2. Find which field(s) actually distinguish main agent from sub-agents
  3. Design Approach 5 based on actual observed behavior

---

FILES TO CHANGE FOR APPROACH 4 (Quick Reference)

| File | Change |
|------|--------|
| D:\...\src-write-guard.sh | Replace with APPROACH 4 CANDIDATE script (read agent_type directly from stdin — no state file) |
| D:\...\settings.json | Add src-write-guard.sh to Bash block + REMOVE src-agent-recorder.sh from SubagentStart/SubagentStop (no longer needed) |
| D:\...\src-agent-recorder.sh | No longer needed for this purpose — can delete or repurpose |

---

ONBOARDING FOR NEXT SESSION

If you are continuing this work in a new session, start here:

  1. Read this report: .IDE_Plans/hook-src-write-guard-investigation-report.md
  2. Check current state of settings.json and src-write-guard.sh
  3. Goal: Main agent blocked from src/, all sub-agents allowed
  4. Do NOT repeat Approach 1 — proven wrong
  5. Do NOT repeat Approach 2 — proven broken
  6. Do NOT repeat Approach 3 — more complex than needed, Approach 4 is simpler but FAILED
  7. Do NOT rely on web research for PreToolUse stdin fields — research was incorrect
  8. NEXT STEP: Add debug logging to capture actual PreToolUse stdin structure and find which field distinguishes main vs sub-agent
  9. Design Approach 5 based on actual observed behavior

---

REMAINING OPEN QUESTIONS

| # | Question | Why it matters | Status |
|---|----------|---------------|--------|
| Q1 | Does PreToolUse stdin have agent_type? | ANSWERED: NO — Web research was incorrect | 🔴 CONFIRMED ABSENT |
| Q2 | What field(s) in PreToolUse distinguish main vs sub-agent? | UNKNOWN — Need debug logging | 🟡 PENDING INVESTIGATION |
| Q3 | Can we identify sub-agents via some other field? | Possibly via tool, prompt, or other fields | 🟡 PENDING INVESTIGATION |
| Q4 | Debug approach needed | Need to capture actual PreToolUse stdin | 🟡 PENDING — add debug logging |

---

HISTORY

| Date/Time | Event | Outcome |
|-----------|-------|---------|
| 2026-04-27 AM | Approach 1 (UserPromptSubmit) attempted | 🔴 FAILED — wrong event type |
| 2026-04-27 PM | Approach 2 (SubagentStart/Stop) applied | 🔴 FAILED — SubagentStop clears state before sub-agent writes |
| 2026-04-27 PM | gas-script-coder delegated and blocked | Confirmed approach 2 is fully broken |
| 2026-04-27 PM | Objective changed — allow all sub-agents | New approach needed |
| 2026-04-27 PM | Web research: PreToolUse stdin structure | ✅ FOUND: agent_type is in stdin for sub-agents, absent for main agent |
| 2026-04-27 PM | Approach 4 (direct stdin read) designed | 🟢 SIMPLEST — no state file needed |
| 2026-04-30 | Approach 4 tested | 🔴 FAILED — agent_type NOT in PreToolUse stdin (web research was incorrect) |

---

---

DETAILED APPROACHES

---

APPROACH 1 — UserPromptSubmit Recorder 🔴 FAILED

Intent: When user types message, record agent_type to state file. PreToolUse reads state file to know who is calling.

Files changed: settings.json (UserPromptSubmit hook), src-agent-recorder.sh

Result: FAILED — Completely broken

Why It Failed

  • UserPromptSubmit fires when USER types a message — not when an agent runs
  • The stdin for UserPromptSubmit does NOT contain the running agent's type
  • agent_type was always null or "main-agent" even when a sub-agent like gas-script-coder was running
  • State file always said "main-agent" regardless of which agent actually called Write/Edit
  • ALL agents (including approved ones) got blocked

Exact Error Observed

src-write-guard.sh: [HOOK BLOCKED] Only gas-script-coder and gas-script-coder agents can write to src/ folder. Your agent: 'main-agent' is not authorized.

Lesson learned: UserPromptSubmit is for user input events, NOT agent execution events. Never use it to track which agent is running.

---

APPROACH 2 — SubagentStart/SubagentStop Recorder 🔴 FAILED

Intent: When a sub-agent starts, record its type to state file. When it stops, clear state. PreToolUse reads state to know who is calling.

Files changed: settings.json (SubagentStart/SubagentStop hooks), src-agent-recorder.sh (updated)

Result: FAILED — Completely broken — even approved agents cannot write

What Happened

1. User asked main agent to create src/02_Triggers/readme.md
2. Main agent was blocked (as expected)

3. User delegated to gas-script-coder sub-agent
4. gas-script-coder tried to write src/02_Triggers/readme.md
5. gas-script-coder was ALSO BLOCKED ← NEW FINDING

The Timing Issue (Root Cause)

Main agent spawns gas-script-coder
  → SubagentStart fires → state = "gas-script-coder" ✓

gas-script-coder asks user: "Should I create readme.md?"
  → User says "yes"

  → SubagentStop fires at this point (gas-script-coder finished its "turn")
  → state file cleared ✗

gas-script-coder actually writes the file
  → PreToolUse fires → reads state file → EMPTY → BLOCKED ✗

SubagentStop fires BEFORE the sub-agent completes its actual work — because asking for confirmation is treated as the end of that turn.

Lesson learned: SubagentStop fires when the sub-agent asks for confirmation, NOT when it completes its task after confirmation.

---

APPROACH 3 — New Logic: Empty State = Block, Non-Empty = Allow 🟡 READY FOR TESTING (BUT APPROACH 4 IS SIMPLER)

Intent: Remove exact-type checking. Use simple logic: state file has content = sub-agent running = ALLOW. State file is empty = main agent = BLOCK.

Files to change: settings.json, src-write-guard.sh

Result: NOT YET TESTED — More complex than Approach 4

What Needs to Change

  1. Add src-write-guard.sh to Bash block — closes the retry bypass gap
  2. Add SessionEnd hook — clears state file so no stale state across sessions
  3. Simplify src-write-guard.sh logic — empty state = block, non-empty = allow

Why Approach 3 is now OBSOLETE

Approach 4 (below) achieves the same goal without ANY state file — just read agent_type directly from PreToolUse stdin. Simpler, fewer failure points.

---

APPROACH 4 — Direct agent_type Read from PreToolUse stdin 🔴 TESTED — FAILED

Intent: No state file needed. PreToolUse stdin already has agent_type for sub-agents but NOT for main agent. Use this directly to distinguish main agent from sub-agents.

Files to change: settings.json, src-write-guard.sh

Result: 🔴 TESTED AND FAILED — agent_type does NOT exist in PreToolUse stdin for any agent type

What Was Tested

  1. Deployed Approach 4 script to .claude/hooks/enforcement/src-write-guard.sh
  2. Tested with gas-script-coder sub-agent
  3. Sub-agent was BLOCKEDagent_type field was absent (empty) in PreToolUse stdin

Why It Failed

Web research was incorrect. The agent_type field does NOT appear in PreToolUse stdin for any agent type (main or sub-agent). The actual PreToolUse JSON structure is different from what was documented online.

How It Works

PreToolUse fires (for Write, Edit, or Bash to src/)
  → src-write-guard.sh reads stdin JSON
  → Check if agent_type field is present
  → If ABSENT → main agent → BLOCK
  → If PRESENT → sub-agent → check if in approved list → ALLOW or BLOCK

Why This Works

From Claude Code hooks documentation (verified via web search):

  • Sub-agents: PreToolUse stdin includes "agent_type": "gas-script-coder" (or whatever type)
  • Main agent: PreToolUse stdin does NOT have agent_type field at all (absent, not "main-agent")
  • So: agent_type absent = main agent = BLOCK, agent_type present = sub-agent = check approval

Behavior Matrix

| Agent | agent_type in PreToolUse stdin | Action |
|-------|--------------------------------|--------|
| Main agent | ABSENT | BLOCK |
| gas-script-coder | YES — "gas-script-coder" | ALLOW |
| gas-sheets-builder | YES — "gas-sheets-builder" | ALLOW |
| git-flow-expert | YES — "git-flow-expert" | BLOCK (not in approved list) |
| gas-error-handler | YES — "gas-error-handler" | BLOCK (not in approved list) |

---

CODE CANDIDATE — Approach 4 (BEST)

File: src-write-guard.sh (APPROACH 4 — NO STATE FILE NEEDED)

#!/bin/bash
# src-write-guard.sh
# PreToolUse hook: restricts src/ folder writes to only approved sub-agents
#
# Logic (NO state file needed):
#   - agent_type IN stdin -> sub-agent is running -> use it directly
#   - agent_type NOT in stdin -> main agent -> BLOCK
#   - Only gas-script-coder and gas-sheets-builder are allowed
#
# Runs on: Edit, Write, AND Bash (catches retry bypass)

INPUT=$(cat)
if [ -z "$INPUT" ]; then
    exit 0
fi

# Extract file_path from PreToolUse JSON
TOOL_INPUT_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [ -z "$TOOL_INPUT_PATH" ] || [ "$TOOL_INPUT_PATH" = "empty" ] || [ "$TOOL_INPUT_PATH" = "null" ]; then
    exit 0
fi

# Normalize path: convert Windows backslash to forward slash
NORMALIZED_PATH=$(echo "$TOOL_INPUT_PATH" | sed 's/\\/\//g')

# Check if path contains /src/
if echo "$NORMALIZED_PATH" | grep -q "/src/"; then
    # Extract agent_type from stdin — present ONLY for sub-agents
    AGENT_TYPE=$(echo "$INPUT" | jq -r '.agent_type // empty')

    if [ -z "$AGENT_TYPE" ] || [ "$AGENT_TYPE" = "empty" ] || [ "$AGENT_TYPE" = "null" ]; then
        # No agent_type -> main agent (not a sub-agent) -> BLOCK
        echo "[HOOK BLOCKED] Main agent cannot write to src/ folder." >&2
        echo "Only gas-script-coder and gas-sheets-builder sub-agents are allowed." >&2
        exit 2
    fi

    # agent_type is present — check if approved
    if [ "$AGENT_TYPE" = "gas-sheets-builder" ] || [ "$AGENT_TYPE" = "gas-script-coder" ]; then
        exit 0
    else
        echo "[HOOK BLOCKED] Only gas-script-coder and gas-sheets-builder agents can write to src/ folder." >&2
        echo "Your agent: '$AGENT_TYPE' is not authorized." >&2
        exit 2
    fi
fi

exit 0

File: settings.json (APPROACH 4 CANDIDATE)

"UserPromptSubmit": [],
"PreToolUse": [
  {
    "matcher": "Edit|Write",
    "hooks": [
      { "command": "bash \".../gs-file-restriction-enforcer.sh\"" },
      { "command": "bash \".../unit-test-checker.sh\"" },
      { "command": "bash \".../plan-validator.sh\"" },
      { "command": "bash \".../src-write-guard.sh\"" }
    ]
  },
  {
    "matcher": "Agent|Bash|Write|Edit|NotebookEdit|Delete|Move|Copy",
    "hooks": [
      { "command": "bash \".../subagent-delegation-enforcer.sh\"" },
      { "command": "bash \".../global-rules-enforcer.sh\"" },
      { "command": "bash \".../src-write-guard.sh\"" }
      // src-write-guard now runs for Bash too — closes retry bypass
    ]
  }
],
// REMOVE src-agent-recorder.sh from SubagentStart/SubagentStop (no longer needed)
// NO SessionEnd hook needed (no state file to clear)

What to Remove from settings.json

Remove these from SubagentStart and SubagentStop (no longer needed):

- src-agent-recorder.sh
- src-agent-recorder.sh --clear

---

SUMMARY TABLE — All Approaches

| | Approach 1 | Approach 2 | Approach 3 | Approach 4 |
|---|---|---|---|---|
| Uses state file | Yes | Yes | Yes | NO |
| Needs SubagentStart/Stop | Yes | Yes | Yes | NO |
| Reads agent_type from stdin | No | No | No | YES (but field doesn't exist) |
| Bash bypass fixed | No | No | Yes | YES |
| Stale state issue | Yes | Yes | Yes (SessionEnd fixes) | NO |
| Complexity | Medium | High | High | LOW |
| Status | 🔴 FAILED | 🔴 FAILED | 🟡 Not tested | 🔴 TESTED FAILED |

---

FILES TO CHANGE FOR APPROACH 4

| File | Action |
|------|--------|
| src-write-guard.sh | Replace with Approach 4 candidate script above |
| settings.json | 1. Add src-write-guard.sh to Agent\|Bash\|Write\|Edit\|... block |
| settings.json | 2. Remove src-agent-recorder.sh from SubagentStart |
| settings.json | 3. Remove src-agent-recorder.sh --clear from SubagentStop |
| src-agent-recorder.sh | Delete or repurpose (no longer needed for this hook) |

Claude Model

None

Is this a regression?

No, this never worked

Last Working Version

_No response_

Claude Code Version

2.1.123 (Claude Code)

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

VS Code integrated terminal

Additional Information

I have built a fully specialized subagent system:

Main agent → coordinator only, reads everything, writes ONLY to /.IDE_Plans/ for planning documents. Must never touch source code directly.
gas-script-coder → writes Google Apps Script code, but write access restricted to ONE specific folder only. Can READ everything including PRD documents, architecture files, and all other folders. Write access to its assigned folder only — no other folder.
gas-code-review → read-only agent. No write access at all. Only has Read tools. This is already working correctly because read-only is easy to enforce. The problem is write-restricted agents.
gas-testing-agent → read-only agent. No writing access. Only reading tools assigned. Working correctly for the same reason.
gas-error-handler → handles error logging, write access to logs folder only.
gas-sheets-builder → builds sheet structures, write access to its specific assigned folder only. Can read everything but write only to its designated folder.

The pattern across all agents:
Every agent can READ everything — PRD documents, plans, all source folders. But each agent can WRITE only to its ONE assigned folder. No agent can write to another agent's folder. This gives full isolation, full context awareness, and zero cross-contamination between agents.
The critical broken behavior:
When I block the main agent's write access to force it to delegate to subagents — ALL subagents also lose their write access simultaneously. There is no way to block only the main agent. The permission system is session-wide, not agent-wide. Blocking the main agent means blocking everyone. This makes the entire specialized architecture collapse — the very agents that SHOULD be writing cannot write, and the main agent that SHOULD NOT be writing still does it anyway when restrictions are removed. There is no middle ground available today.

View original on GitHub ↗

This issue has 7 comments on GitHub. Read the full discussion on GitHub ↗