Development skill + hook

One-way door check

Flag irreversible architectural decisions before you commit to them. The most expensive mistakes aren't bugs — they're decisions that can't be undone.

The concept

One-way doors

Decisions that create gravity. Once traffic, users, or other code depends on them, changing course gets expensive.

  • Database schema after launch
  • API contract with external consumers
  • Auth model shaping permissions
  • Infrastructure stack your team learns

Two-way doors

Decisions you can reverse easily. Walk through, and if it's wrong, walk back. No lasting consequences.

  • UI components and styling
  • Utility functions and helpers
  • Test infrastructure
  • Documentation and logging

This skill teaches Claude to recognize the difference and pause before one-way doors. The companion hook automates enforcement by blocking file creation for known architectural patterns until you've discussed the trade-offs.

What gets flagged

Data models

Schemas, migrations, entity definitions. Once your database has rows, every change requires a migration.

Infrastructure

Docker, Terraform, Kubernetes, Helm. Infrastructure choices constrain everything built on top.

Auth boundaries

Security rules, RBAC, permissions. Auth boundaries are load-bearing walls in your architecture.

API contracts

OpenAPI specs, protobuf definitions, route files. Published APIs are promises to consumers.

Event systems

Pub/sub, message queues, event buses. Event schemas are contracts between producers and consumers.

CI/CD pipelines

GitHub Actions, GitLab CI, Jenkins. Pipelines become the backbone of your release process.

Dependencies

package.json, Cargo.toml, go.mod. Framework choices ripple through your entire codebase.

Cloud configs

Firebase, Firestore indexes. Cloud service configs lock you into specific providers and architectures.

How it works

Detect

Hook intercepts file creation

The PreToolUse hook fires on every Write call. It extracts the file path and checks it against known one-way-door filename patterns.

Block

One-way door detected

If the file matches a one-way-door pattern and isn't already approved this session, the hook records it as pending, exits with code 2 (block), and sends a message to stderr explaining what was caught and why.

Ask

Claude discusses trade-offs

Claude uses AskUserQuestion to present the architectural decision: what it does, alternative approaches, and trade-offs. The user decides how to proceed.

Proceed

Approve, then write with confidence

When the user answers, a companion hook (PostToolUse:AskUserQuestion) promotes the file to approved for the session, so Claude's retried write passes instead of re-blocking. Two-way door files always pass through silently, and a new session starts with a clean slate.

The hook scripts

Two shell scripts that share a session-scoped approval ledger. The check runs on every Write: it pattern-matches the filename and blocks with exit code 2 if the file is a one-way door it hasn't already seen approved this session.

#!/bin/sh
INPUT=$(cat)
[ -z "$INPUT" ] && exit 0

FILE_PATH=$(echo "$INPUT" | grep -oP '"file_path"\s*:\s*"[^"]*"' \
  | head -1 | sed 's/.*"file_path"\s*:\s*"//;s/"//')
[ -z "$FILE_PATH" ] && exit 0

# Session-scoped approval ledger
SESSION_ID=$(echo "$INPUT" | grep -oP '"session_id"\s*:\s*"[^"]*"' \
  | head -1 | sed 's/.*"session_id"\s*:\s*"//;s/"//')
[ -z "$SESSION_ID" ] && SESSION_ID="default"
STATE_DIR="$HOME/.claude/hooks/state/one-way-door"; mkdir -p "$STATE_DIR"
APPROVED_FILE="$STATE_DIR/$SESSION_ID.approved"
PENDING_FILE="$STATE_DIR/$SESSION_ID.pending"

# Already approved this session: note it and allow
if [ -f "$APPROVED_FILE" ] && grep -Fxq "$FILE_PATH" "$APPROVED_FILE"; then
    echo "one-way-door: proceeding with previously-approved $(basename "$FILE_PATH")" >&2
    exit 0
fi

FILENAME=$(basename "$FILE_PATH")
FILENAME_LOWER=$(echo "$FILENAME" | tr "[:upper:]" "[:lower:]")
FILE_PATH_LOWER=$(echo "$FILE_PATH" | tr "[:upper:]" "[:lower:]")
DIR=$(dirname "$FILE_PATH")

# Early-exit safelist: always-reversible classes pass even if the name
# contains a keyword like "auth" (tests, fixtures/mocks, markdown, docs)
if echo "$FILENAME_LOWER" | grep -qE \
  "^test_.*\.py$|_test\.py$|\.test\.(ts|tsx|js|jsx)$|\.spec\.(ts|tsx|js|jsx)$"; then
    exit 0
fi
if echo "$FILE_PATH_LOWER" | grep -qE \
  "/tests?/|/__tests__/|/fixtures?/|/mocks?/|/__mocks__/"; then
    exit 0
fi
if echo "$FILENAME_LOWER" | grep -qE "\.md$"; then exit 0; fi
if echo "$FILENAME_LOWER" | grep -qE "\.txt$|\.rst$"; then
    echo "$DIR" | grep -qE "plans?|docs?|notes?|superpowers" && exit 0
fi

ONE_WAY=0
REASON=""

# Data models, infra, auth, APIs, events, deps, cloud, CI/CD
if echo "$FILENAME_LOWER" | grep -qE \
  "schema\.(prisma|graphql|sql)|migration|\.sql$|models?\.(py|ts|js)$"; then
    ONE_WAY=1; REASON="data model / database schema"
fi

if echo "$FILENAME_LOWER" | grep -qE \
  "^(docker-compose|dockerfile|terraform|pulumi|cdk)|\.tf$"; then
    ONE_WAY=1; REASON="infrastructure config"
fi

if echo "$FILENAME_LOWER" | grep -qE \
  "auth\.(ts|js|py)|\.rules$|security\.(ts|js|py|json|rules|yaml|yml)|rbac\.(ts|js|py|json)|permissions\.(ts|js|py|json)"; then
    ONE_WAY=1; REASON="auth / security rules"
fi

# ... (8 categories total)

if [ "$ONE_WAY" = "1" ]; then
    grep -Fxq "$FILE_PATH" "$PENDING_FILE" 2>/dev/null \
      || printf '%s\n' "$FILE_PATH" >> "$PENDING_FILE"  # record as pending
    echo "ONE_WAY_DOOR: $FILENAME ($REASON)" >&2
    echo "Blocked. Use AskUserQuestion, then retry." >&2
    exit 2  # Block the write
fi

exit 0  # Allow two-way doors

A second hook runs on PostToolUse:AskUserQuestion. When the user answers any AskUserQuestion, it promotes every pending path to approved so the retried write passes. It never blocks. It keys on the event, not the specific question — so an unrelated question answered while a file is pending approves it too. The block-then-discuss prompt is the guardrail; the ledger just stops an already-discussed file from re-blocking.

#!/bin/sh
# one-way-door-approve.sh (PostToolUse:AskUserQuestion)
INPUT=$(cat); [ -z "$INPUT" ] && exit 0

SESSION_ID=$(echo "$INPUT" | grep -oP '"session_id"\s*:\s*"[^"]*"' \
  | head -1 | sed 's/.*"session_id"\s*:\s*"//;s/"//')
[ -z "$SESSION_ID" ] && SESSION_ID="default"
STATE_DIR="$HOME/.claude/hooks/state/one-way-door"; mkdir -p "$STATE_DIR"
APPROVED_FILE="$STATE_DIR/$SESSION_ID.approved"
PENDING_FILE="$STATE_DIR/$SESSION_ID.pending"

[ -s "$PENDING_FILE" ] || exit 0  # nothing pending

# Promote each pending path into approved, deduped
while IFS= read -r path; do
    [ -z "$path" ] && continue
    grep -Fxq "$path" "$APPROVED_FILE" 2>/dev/null \
      || printf '%s\n' "$path" >> "$APPROVED_FILE"
done < "$PENDING_FILE"

: > "$PENDING_FILE"  # clear pending now that it's approved
exit 0

Full check script with all 8 categories, plus the approval hook, in the skill directory.

Two-way doors

These file types pass through the hook silently. They're safe to decide quickly and change later. Test files, Markdown, and docs/plans text files are enforced by an early-exit safelist, so they pass even when the name contains a keyword like "auth".

UI components

Utilities

Test files

Documentation

Styling / CSS

Logging

Static assets

App config

Installation

Option 1: Install the skill

# Recommended: install the dev-toolkit plugin

/plugin marketplace add jamditis/claude-skills-journalism

/plugin install dev-toolkit@claude-skills-journalism

# Or copy just this skill from the plugin tree

git clone https://github.com/jamditis/claude-skills-journalism.git

cp -r claude-skills-journalism/dev-toolkit/skills/one-way-door ~/.claude/skills/

Option 2: Add the CLAUDE.md rule

Add this paragraph to your project's CLAUDE.md for prompt-based enforcement (no hook needed):

### One-way door check
Before creating new files that represent architectural
decisions, ask: "Which of these decisions would be
difficult to reverse?" One-way doors include data models,
service communication patterns, auth boundaries, tenancy
models, and infrastructure configs. If a decision is a
one-way door, pause and discuss the trade-offs before
committing.

Option 3: Add the automated hooks

For automated enforcement, save both scripts and add this to your settings.json. The approval hook is required — without it, the check re-blocks an approved file on every retry:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Write",
      "hooks": [{
        "type": "command",
        "command": "/path/to/one-way-door-check.sh"
      }]
    }],
    "PostToolUse": [{
      "matcher": "AskUserQuestion",
      "hooks": [{
        "type": "command",
        "command": "/path/to/one-way-door-approve.sh"
      }]
    }]
  }
}

Windows (PowerShell)

On Windows, hooks run through PowerShell and file paths can use backslashes. Behavior-matched ports — one-way-door-check.ps1 and one-way-door-approve.ps1 — ship in the skill directory and use the same session ledger, safelist, and categories. Copy both into your hooks folder (for example %USERPROFILE%\.claude\hooks\), then wire them with the PowerShell launcher — update the paths to match where you saved them (replace <you>):

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Write",
      "hooks": [{
        "type": "command",
        "command": "powershell -ExecutionPolicy Bypass -File C:/Users/<you>/.claude/hooks/one-way-door-check.ps1"
      }]
    }],
    "PostToolUse": [{
      "matcher": "AskUserQuestion",
      "hooks": [{
        "type": "command",
        "command": "powershell -ExecutionPolicy Bypass -File C:/Users/<you>/.claude/hooks/one-way-door-approve.ps1"
      }]
    }]
  }
}

Or browse this skill in the GitHub repository.

Related skills

Measure twice, cut once

Pause before irreversible decisions. A five-minute conversation about trade-offs saves weeks of migration pain.

View on GitHub