---
status: implemented
date: 2025-12-09
decision-maker: Terry Li
consulted:
  - Plan Mode (workflow design)
research-method: single-agent
clarification-iterations: 1
perspectives: [Usability, Architecture, Backward Compatibility]
---

# Workflow-Aware Graph-Easy Detection for PreToolUse Hook

**Design Spec**: [Implementation Spec](/docs/design/2025-12-09-itp-hooks-workflow-aware-graph-easy/spec.md)

## Context and Problem Statement

The `pretooluse-guard.sh` hook blocks writes to markdown files containing ASCII box-drawing characters (≥10 chars) without a `<summary>graph-easy source</summary>` block. This enforcement ensures reproducible diagrams.

However, this creates a **workflow friction** when using graph-easy correctly:

1. Claude runs `graph-easy` via Bash → gets rendered ASCII output
2. Claude uses Write/Edit to add that output to markdown file
3. PreToolUse hook sees box-drawing characters, no source block yet → **BLOCKS**

This is incorrect because the content **legitimately came from graph-easy**, but the source block hasn't been assembled yet. The hook has no way to know the ASCII art was just generated by graph-easy.

### Before/After Diagram

**Before**: Graph-easy output blocked despite being legitimate

```
 ⏮️ Before: Graph-Easy Output Blocked

         ╭───────────────────╮
         │      Claude       │
         ╰───────────────────╯
           │
           │
           ∨
         ┌───────────────────┐
         │  Run graph-easy   │
         └───────────────────┘
           │
           │
           ∨
         ┌───────────────────┐
         │ Write to Markdown │
         └───────────────────┘
           │
           │
           ∨
         ┌───────────────────┐
         │  PreToolUse Hook  │
         └───────────────────┘
           │
           │
           ∨
         ┌───────────────────┐
         │    ASCII Check    │
         └───────────────────┘
           │
           │ no source block
           ∨
         ╔═══════════════════╗
         ║      BLOCKED      ║
         ╚═══════════════════╝
```

<details>
<summary>graph-easy source</summary>

```
graph { label: "⏮️ Before: Graph-Easy Output Blocked"; flow: south; }
[ Claude ] { shape: rounded; }
[ Run graph-easy ]
[ Write to Markdown ]
[ PreToolUse Hook ]
[ ASCII Check ]
[ BLOCKED ] { border: double; }

[ Claude ] -> [ Run graph-easy ]
[ Run graph-easy ] -> [ Write to Markdown ]
[ Write to Markdown ] -> [ PreToolUse Hook ]
[ PreToolUse Hook ] -> [ ASCII Check ]
[ ASCII Check ] -- no source block --> [ BLOCKED ]
```

</details>

**After**: Workflow-aware detection allows legitimate graph-easy output

```
 ⏭️ After: Workflow-Aware Detection

        ╭───────────────────╮
        │      Claude       │
        ╰───────────────────╯
          │
          │
          ∨
        ┌───────────────────┐
        │  Run graph-easy   │
        └───────────────────┘
          │
          │
          ∨
        ┌───────────────────┐
        │ PostToolUse Hook  │
        └───────────────────┘
          │
          │
          ∨
        ┌───────────────────┐
        │    Write Flag     │
        └───────────────────┘
          │
          │
          ∨
        ┌───────────────────┐
        │ Write to Markdown │
        └───────────────────┘
          │
          │
          ∨
        ┌───────────────────┐
        │  PreToolUse Hook  │
        └───────────────────┘
          │
          │
          ∨
        ┌───────────────────┐
        │    Check Flag     │
        └───────────────────┘
          │
          │ flag exists
          ∨
        ╭───────────────────╮
        │      ALLOWED      │
        ╰───────────────────╯
```

<details>
<summary>graph-easy source</summary>

```
graph { label: "⏭️ After: Workflow-Aware Detection"; flow: south; }
[ Claude ] { shape: rounded; }
[ Run graph-easy ]
[ PostToolUse Hook ]
[ Write Flag ]
[ Write to Markdown ]
[ PreToolUse Hook ]
[ Check Flag ]
[ ALLOWED ] { shape: rounded; }

[ Claude ] -> [ Run graph-easy ]
[ Run graph-easy ] -> [ PostToolUse Hook ]
[ PostToolUse Hook ] -> [ Write Flag ]
[ Write Flag ] -> [ Write to Markdown ]
[ Write to Markdown ] -> [ PreToolUse Hook ]
[ PreToolUse Hook ] -> [ Check Flag ]
[ Check Flag ] -- flag exists --> [ ALLOWED ]
```

</details>

## Decision Drivers

- **Workflow continuity**: Using graph-easy → writing output should not be interrupted
- **Security preservation**: Manual ASCII art (not from graph-easy) should still be blocked
- **Minimal overhead**: Inter-hook communication must be fast (~5ms budget)
- **Session isolation**: Flag files must be scoped to session to prevent cross-session leakage

## Considered Options

1. **Disable blocking entirely** — Too permissive, loses enforcement
2. **Transcript-based detection** — Check if graph-easy appears in recent tool calls (false positives)
3. **Session flag file** — PostToolUse writes flag, PreToolUse checks it
4. **Environment variable** — Pass state via env (doesn't persist across tool calls)

## Decision Outcome

**Chosen option**: "Session flag file with timestamp expiry"

PostToolUse writes a session-scoped flag file when graph-easy is detected. PreToolUse checks for this flag before blocking, allowing the write if the flag exists and is fresh (< 30 seconds).

### Implementation

**State Directory**: `~/.claude/hooks/state/`

**Flag File**: `{session_id}.graph-easy-used` containing Unix timestamp

**PostToolUse** (write flag):

```bash
if [[ "$COMMAND" =~ graph-easy ]]; then
    STATE_DIR="$HOME/.claude/hooks/state"
    mkdir -p "$STATE_DIR"
    SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""')
    if [[ -n "$SESSION_ID" ]]; then
        echo "$(date +%s)" > "$STATE_DIR/${SESSION_ID}.graph-easy-used"
    fi
fi
```

**PreToolUse** (check flag):

```bash
STATE_DIR="$HOME/.claude/hooks/state"
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""')
if [[ -n "$SESSION_ID" && -f "$STATE_DIR/${SESSION_ID}.graph-easy-used" ]]; then
    FLAG_TIMESTAMP=$(cat "$STATE_DIR/${SESSION_ID}.graph-easy-used")
    CURRENT_TIME=$(date +%s)
    FLAG_AGE=$((CURRENT_TIME - FLAG_TIMESTAMP))
    if [[ $FLAG_AGE -lt 30 ]]; then
        rm "$STATE_DIR/${SESSION_ID}.graph-easy-used"
        exit 0  # Allow write
    fi
    rm "$STATE_DIR/${SESSION_ID}.graph-easy-used"  # Expired, clean up
fi
```

## Architecture

```
🏗️ Architecture: Inter-Hook Communication via Session Flags

╭────────────╮     ┌─────────────┐  writes flag   ┌───────────┐
│ Bash Tool  │ ──> │ PostToolUse │ ─────────────> │ State Dir │
╰────────────╯     └─────────────┘                └───────────┘
                                                    ∧
                                                    │
                                                    │
╭────────────╮     ┌─────────────┐  reads flag      │
│ Write Tool │ ──> │ PreToolUse  │ ─────────────────┘
╰────────────╯     └─────────────┘
                     │
                     │
                     ∨
                   ┌─────────────┐
                   │ Allow/Block │
                   └─────────────┘
```

<details>
<summary>graph-easy source</summary>

```
graph { label: "🏗️ Architecture: Inter-Hook Communication via Session Flags"; flow: east; }
[ Bash Tool ] { shape: rounded; }
[ PostToolUse ]
[ State Dir ]
[ Write Tool ] { shape: rounded; }
[ PreToolUse ]
[ Allow/Block ]

[ Bash Tool ] -> [ PostToolUse ]
[ PostToolUse ] -- writes flag --> [ State Dir ]
[ Write Tool ] -> [ PreToolUse ]
[ PreToolUse ] -- reads flag --> [ State Dir ]
[ PreToolUse ] -> [ Allow/Block ]
```

</details>

### Consequences

**Good**:

- Natural workflow preserved: graph-easy → write works without blocking
- Session-scoped: no cross-session interference
- Time-limited: 30-second window prevents stale flags from causing issues
- One-shot: flag consumed after use, preventing abuse

**Neutral**:

- Adds filesystem I/O (~1-2ms for write, ~1ms for read)
- New state directory to manage

**Bad**:

- Requires `session_id` in hook input (must verify availability)
- Stale files could accumulate if sessions crash (mitigated by cleanup)

## More Information

- **Related ADR**: [Plan File Exemption](/docs/adr/2025-12-09-itp-hooks-plan-file-exemption.md) — Similar exemption pattern
- **Pattern**: Uses filesystem as IPC mechanism between hooks (common Unix pattern)
- **Cleanup**: SessionStart hook can clean stale files older than 1 hour
