claude-code - 💡(How to fix) Fix [BUG] Claude Code — Per-Agent Permission Control Gap [2 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#54898Fetched 2026-05-01 05:51:33
View on GitHub
Comments
2
Participants
2
Timeline
10
Reactions
0
Timeline (top)
labeled ×7commented ×2unlabeled ×1

Error Message

gas-error-handler → handles error logging gas-error-handler → handles errors in logs/

Error Messages/Logs

Exact Error Observed

| gas-error-handler | YES — "gas-error-handler" | BLOCK (not in approved list) |

Exact Error Observed

| gas-error-handler | YES — "gas-error-handler" | BLOCK (not in approved list) | gas-error-handler → handles error logging, write access to logs folder only.

Root Cause

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.

Code Example

What We Tried & Why Each Failed
Attempt 1Global 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 2Path-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 3Subagent 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 4Separate 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 5PreToolUse 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 FAILEDDebug 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.

---

## SUMMARYAll 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) | 🔴 **TESTEDFAILED** |

### 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 4TESTED 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 | 🔴 FAILEDSubagentStop 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 1UserPromptSubmit 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:** FAILEDCompletely 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 2SubagentStart/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:** FAILEDCompletely 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 BLOCKEDNEW 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 → EMPTYBLOCKED

**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 3New 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 TESTEDMore 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 4Direct agent_type Read from PreToolUse stdin 🔴 TESTEDFAILED

**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 CANDIDATEApproach 4 (BEST)

**File: src-write-guard.sh (APPROACH 4NO 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 TABLEAll 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) |

---

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.

---

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 BLOCKEDNEW FINDING

---

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 → EMPTYBLOCKED
---

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

---

#!/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

---

"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)

---

- src-agent-recorder.sh
- src-agent-recorder.sh --clear
RAW_BUFFERClick to expand / collapse

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • 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

Actorsrc/ 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 1Approach 2Approach 3Approach 4
Recorder eventUserPromptSubmitSubagentStart/StopSubagentStart/Stop + SessionEndNONE — no state file
Guard eventEdit|WriteEdit|WriteEdit|Write + BashEdit|Write|Bash
State file logicexact type matchexact type matchempty = block, non-empty = allowNO STATE FILE — tried agent_type from stdin
Bash bypass fixed?NoNoYESYES
Stale state fixed?NoNoYESYES
Works for approved agents?NoNoYESNO — 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)

FileChange
D:\...\src-write-guard.shReplace with APPROACH 4 CANDIDATE script (read agent_type directly from stdin — no state file)
D:\...\settings.jsonAdd src-write-guard.sh to Bash block + REMOVE src-agent-recorder.sh from SubagentStart/SubagentStop (no longer needed)
D:\...\src-agent-recorder.shNo 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

#QuestionWhy it mattersStatus
Q1Does PreToolUse stdin have agent_type?ANSWERED: NO — Web research was incorrect🔴 CONFIRMED ABSENT
Q2What field(s) in PreToolUse distinguish main vs sub-agent?UNKNOWN — Need debug logging🟡 PENDING INVESTIGATION
Q3Can we identify sub-agents via some other field?Possibly via tool, prompt, or other fields🟡 PENDING INVESTIGATION
Q4Debug approach neededNeed to capture actual PreToolUse stdin🟡 PENDING — add debug logging

HISTORY

Date/TimeEventOutcome
2026-04-27 AMApproach 1 (UserPromptSubmit) attempted🔴 FAILED — wrong event type
2026-04-27 PMApproach 2 (SubagentStart/Stop) applied🔴 FAILED — SubagentStop clears state before sub-agent writes
2026-04-27 PMgas-script-coder delegated and blockedConfirmed approach 2 is fully broken
2026-04-27 PMObjective changed — allow all sub-agentsNew approach needed
2026-04-27 PMWeb research: PreToolUse stdin structure✅ FOUND: agent_type is in stdin for sub-agents, absent for main agent
2026-04-27 PMApproach 4 (direct stdin read) designed🟢 SIMPLEST — no state file needed
2026-04-30Approach 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

Agentagent_type in PreToolUse stdinAction
Main agentABSENTBLOCK
gas-script-coderYES — "gas-script-coder"ALLOW
gas-sheets-builderYES — "gas-sheets-builder"ALLOW
git-flow-expertYES — "git-flow-expert"BLOCK (not in approved list)
gas-error-handlerYES — "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 1Approach 2Approach 3Approach 4
Uses state fileYesYesYesNO
Needs SubagentStart/StopYesYesYesNO
Reads agent_type from stdinNoNoNoYES (but field doesn't exist)
Bash bypass fixedNoNoYesYES
Stale state issueYesYesYes (SessionEnd fixes)NO
ComplexityMediumHighHighLOW
Status🔴 FAILED🔴 FAILED🟡 Not tested🔴 TESTED FAILED

FILES TO CHANGE FOR APPROACH 4

FileAction
src-write-guard.shReplace with Approach 4 candidate script above
settings.json1. Add src-write-guard.sh to Agent|Bash|Write|Edit|... block
settings.json2. Remove src-agent-recorder.sh from SubagentStart
settings.json3. Remove src-agent-recorder.sh --clear from SubagentStop
src-agent-recorder.shDelete 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.

extent analysis

TL;DR

The most likely fix involves modifying the src-write-guard.sh script to correctly identify and allow writes from approved sub-agents while blocking the main agent, potentially by finding an alternative method to distinguish between main and sub-agents since the agent_type field in PreToolUse stdin does not exist as previously thought.

Guidance

  1. Investigate Alternative Identification Methods: Since the agent_type field is not present in PreToolUse stdin, explore other fields or methods that could be used to distinguish between the main agent and sub-agents. This might involve deeper inspection of the PreToolUse stdin structure or utilizing other hooks/events.
  2. Debug Logging for PreToolUse Stdin: Implement debug logging to capture the actual structure and content of PreToolUse stdin. This will help in understanding what information is available and how it can be used to identify the calling agent.
  3. Review Claude Code Documentation and Community Resources: Given that web research provided incorrect information about the agent_type field, review official Claude Code documentation and community forums for accurate information on distinguishing between main and sub-agents within hooks.
  4. Consider Custom Solutions or Workarounds: If standard methods do not provide a straightforward solution, consider developing custom scripts or workarounds that can achieve the desired permission control. This might involve using external tools or scripts that can better manage agent permissions.

Example

Given the complexity and the incorrect assumption about the agent_type field, a direct code example cannot be provided without further investigation into the actual structure of PreToolUse stdin and the capabilities of Claude Code's hook system. However, the approach would involve:

  • Capturing and logging the PreToolUse stdin to understand its structure.
  • Identifying a unique identifier or method to distinguish between main and sub-agents.
  • Modifying the src-write-guard.sh script to use this identifier to allow or block writes accordingly.

Notes

  • The solution requires a

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix [BUG] Claude Code — Per-Agent Permission Control Gap [2 comments, 2 participants]