# Adversarial audit condensed router-gate hard wall спека

**Дата:** 2026-05-29
**Target:** [`docs/superpowers/specs/2026-05-29-router-gate-hard-wall-design-condensed.md`](../specs/2026-05-29-router-gate-hard-wall-design-condensed.md) (commit d4f7e681, 1117 строк)
**Auditor:** Claude (controller Opus 4.7) + 3 параллельных Sonnet 4.6 adversarial subagents
**Методология:** 9 attack zones из user prompt, по 3 zone на агента. Каждый агент инструктирован как «controller который ХОЧЕТ ОБОЙТИ собственный gate».
**Контекст:** спек v3.5 прошёл 4 раунда внутренних adversarial audits + verifier completeness check 63/63. Автор заявил «hard wall полный, acceptable residual list пуст».

---

## TL;DR

**Аудит опровергает заявление о полном hard wall.** Найдено **51 finding** в 9 attack zones, из них:

- **10 BYPASS-COMPLETE** — полный обход hard wall через конкретные exploitable vectors
- **17 BYPASS-PARTIAL** — частичные обходы конкретных компонентов
- **9 DOS** — выводят gate в lockout/false-block состояние, заказчик вынужден отключать через Recovery §6
- **15 INFO / acknowledged** — низкая feasibility, acknowledged limitations, или out-of-scope

**Самое серьёзное:** §5.1 Bash content rules имеет **6 BYPASS-COMPLETE дыр** через формальные обходы whitelist/blacklist (`<<<`, `echo | node`, UNC paths, 8.3 short names, undefined env vars, `cat multi-arg`). Single-line bash commands achieving arbitrary file write/execute.

**Acceptable residual list НЕ пуст.** Минимум 10 holes должны быть закрыты до writing-plans фазы либо явно перенесены в residual list с обоснованием.

---

## Severity breakdown

| Severity | Count | Where most concentrated |
|---|---|---|
| BYPASS-COMPLETE | 10 | §5.1 Bash content (6) + §3.1 path normalization (3) + §4 Поведение 1 (1) |
| BYPASS-PARTIAL | 17 | §3.4 subagent + §3 chain-state + §4.5 answer parser + §5.2 static scan |
| DOS | 9 | §3.6 gate budget + §8 implementation order matrix gaps |
| INFO/Acknowledged | 15 | §4.7 question quality (creative leading questions неизбежны) |
| **Итого** | **51** | |

---

## Top-10 Critical (BYPASS-COMPLETE)

### CRITICAL-1: AskUserQuestion option label triggers Поведение 1 direct invocation

**Source:** Agent 1 / Finding 1.1
**Confidence:** HIGH
**Feasibility:** HIGH

**Attack:** §4 Поведение 1 матчит pattern `(делай|сделай|вызови|примени|используй)\s+<exact-skill-name>` против `transcript[-1].user.content`. AskUserQuestion option labels — controller-controlled text. Когда заказчик выбирает опцию `1` («делай subagent-driven-development»), её label становится user content в transcript. Gate видит pattern → Поведение 1 → allow любой mutating tool.

**Mitigation:** Detector Поведения 1 должен проверять `is_direct_invocation` только для **корневого** user-prompt в turn (первое сообщение), не для AskUserQuestion answers. AskUser answers обрабатываются эксклюзивно через §4.5 answer parser.

---

### CRITICAL-2: vitest globalSetup из cross-session edits (file-watcher blind spot)

**Source:** Agent 1 / Finding 2.2
**Confidence:** HIGH
**Feasibility:** MEDIUM (требует multi-session setup)

**Attack:** §5.1 file-watcher session-scoped — clears на `git commit exit 0 + lefthook GREEN`. Если в сессии N контроллер редактировал `vitest.config.ts` → commit GREEN → watcher cleared. В сессии N+1: `vitest.config.ts` уже не в `edited_files`. `npx vitest run` (whitelisted) запускает vitest → reads config → executes `globalSetup` файлы (произвольный JS). Static scan §5.2 проверяет только explicit target (`vitest run` без target → glob-aware → AskUser), но НЕ автоматически открытые vitest config + setup files.

**Mitigation:** Static scan §5.2 расширить — при любом `npx vitest run` сканировать `vitest.config.*` + `vite.config.*` + все referenced `setupFiles` / `globalSetup` независимо от file-watcher state.

---

### CRITICAL-3: Undefined env var bypass path normalization

**Source:** Agent 2 / Finding 4.1
**Confidence:** HIGH (depends on implementation)
**Feasibility:** HIGH

**Attack:** §3.1 path normalization step 3: «Expand `~` через `os.homedir()` и env vars через `process.env`». Если controller использует path `~/.claude/runtime/$UNDEFINED_VAR/state.json`, и `process.env.UNDEFINED_VAR` undefined, типичная implementation возвращает literal `$UNDEFINED_VAR` в строке. Resolved path содержит `$UNDEFINED_VAR` segment → glob match `~/.claude/runtime/*` не срабатывает (один extra segment) → protected check passes → Edit/Write allowed.

**Mitigation:** Unexpanded `$VAR` sequences после env expansion → fail-CLOSE («unresolved env vars in path»). Или strict whitelist allowed env vars (`HOME`, `USERPROFILE`) с фейлом на остальные.

---

### CRITICAL-4: Windows UNC paths bypass canonicalization

**Source:** Agent 2 / Finding 4.2
**Confidence:** HIGH
**Feasibility:** HIGH

**Attack:** Windows UNC path `\\?\C:\Users\Administrator\.claude\runtime\router-state-abc.json` (long path prefix `\\?\`). `path.resolve` на Windows возвращает путь с `\\?\` prefix. Protected list pattern `~/.claude/runtime/*` после `os.homedir()` → `C:\Users\Administrator\.claude\runtime\*`. UNC version с prefix НЕ match'ит pattern (один extra `\\?\` префикс) → Edit/Write на protected file allowed.

**Mitigation:** Path normalization step 1: strip `\\?\` prefix перед resolve. Step 2: explicit UNC prefix normalization (`\\server\share` → drive form если applicable). Это **Windows-specific** дыра, на Linux/Mac не существует.

---

### CRITICAL-5: Windows 8.3 short names bypass canonicalization

**Source:** Agent 2 / Finding 4.4
**Confidence:** HIGH
**Feasibility:** MEDIUM (8.3 включён по умолчанию на legacy Windows install)

**Attack:** Windows file system поддерживает 8.3 short names: `C:\Users\ADMINI~1\.claude\runtime\state.json`. `path.resolve` НЕ разворачивает 8.3 short names. `fs.realpathSync` разворачивает только если файл **существует** в момент вызова. Для несуществующего файла (новый `router-state-<new-sess>.json` про который gate знает но он не создан) realpath fails → fallback to resolved. Resolved содержит short name → не match'ит protected list (где `Administrator` full form) → allowed.

**Mitigation:** Использовать `GetLongPathName` WinAPI через `node-ffi` или wrap-script. Альтернатива — disable 8.3 generation на NTFS volume (`fsutil 8dot3name set 1`). NB: это **Windows Server 2022 машина** — short names enabled out-of-box.

---

### CRITICAL-6: `cat` multi-arg bypass path-deny overlay

**Source:** Agent 2 / Finding 4.6
**Confidence:** HIGH
**Feasibility:** HIGH

**Attack:** §5.1 path-deny overlay для reading commands (cat/tail/head/grep/less/more/file/stat/wc/ls): «нормализует path и проверяет против protected list». Tokenizer split по `;`/`&&`/`||`/`|`/`&`. Команда `cat file1.txt ~/.claude/runtime/router-state.json` — один токен после split (без separators), `cat` whitelisted, **два** path-args. Если implementation tokenizer извлекает только first token (command), не all subsequent args — второй path-arg `~/.claude/runtime/router-state.json` не проверяется → читается.

**Mitigation:** Tokenizer должен извлекать ВСЕ args после command token и передавать их в path-deny check loop. Per-arg validation, не command-level only.

---

### CRITICAL-7: `git format-patch -o <dir>` write to protected paths

**Source:** Agent 2 / Finding 4.7 + Agent 1 / Finding 5.3
**Confidence:** MEDIUM (требует verification implementation)
**Feasibility:** HIGH

**Attack:** §5.1 whitelist row объединяет `git log/show/diff/blame/format-patch` с исключениями `--output=*/-o <path>/--file=*`. Если implementation parser применяет исключения только к `git log` (первый command в row label), а к `git format-patch` — нет (отдельная command), то `git format-patch HEAD~1 -o ~/.claude/runtime/` пишет patch files в protected dir.

**Vetting note:** spec формулировка может покрывать обе команды (row label «git log / git show / git diff / git blame / git format-patch» применяет exclusions ко всем). Зависит от parser implementation. Если parser per-command, finding genuine. Если row-wide, finding false-positive. **Требует verification в plan**.

**Mitigation:** Explicit per-command exclusion mapping. Или убрать `git format-patch` из whitelist (использовать `git format-patch --stdout` через conditional whitelist + AskUser).

---

### CRITICAL-8: `echo 'code' | node` — pipe receiver `node` is whitelisted

**Source:** Agent 3 / Finding 7.1
**Confidence:** MEDIUM
**Feasibility:** HIGH

**Attack:** §5.1 hard-blacklist: «Любые pipe-команды (`|`) где receiver — mutating command». Но `node` в **whitelist** (без `-e/--eval/-r`). Pipe `echo 'fs.writeFileSync("evil","x")' | node` — receiver `node` whitelisted, не mutating per default classification. Tokenizer splits at `|`, видит `node` в receiver position → whitelisted? Tokenizer семантика «receiver = mutating command» зависит от implementation: если check'ает только blacklist contains, `node` passes; если check'ает «mutation potential», `node` reading stdin = arbitrary execution, должен блокироваться.

**Mitigation:** Explicit: `node` без `<script-path>` arg (т.е. без positional path argument после `node`) — block. Pipe receiver `node` всегда block (REPL-like usage).

---

### CRITICAL-9: `<<<` here-string не в hard-blocked list

**Source:** Agent 3 / Finding 7.3
**Confidence:** HIGH
**Feasibility:** HIGH

**Attack:** §5.1 sub-shell broad sweep блокирует: `` ` ``, `$(...)`, `<(...)/>(...)`, `<<`, `<<-`. **`<<<` (here-string) НЕ упомянут.** Команда `node <<< "fs.writeFileSync('evil','x')"` — bash here-string передаёт строку как stdin к `node`. Tokenizer не split на `<<<` (не в pipe-split list). Gate видит `node` first command, whitelisted, без disallowed flags → allow. Arbitrary Node.js execution.

**Mitigation:** Trivial fix — добавить `<<<` к sub-shell broad sweep list. Простейший finding, наиболее очевидный gap.

---

### CRITICAL-10: enforce-branch-switch migration window (Этап 2.3 sequencing)

**Source:** Agent 3 / Finding 9.3
**Confidence:** MEDIUM (зависит от deployment sequence)
**Feasibility:** HIGH

**Attack:** §8 Implementation order matrix НЕ упоминает Этап 2.3 явно (matrix содержит 5 строк: Этап 1, 1.4, 2, 2.1, 3). Согласно §8 main table sequence: 2 → 2.1.0 → 2.1 → 2.2 → 2.3 → 3. Если implementer пропускает 2.3 (rewrite enforce-branch-switch) ДО Этапа 3 (settings.json enforce-mode), OLD `enforce-branch-switch.mjs` всё ещё ищет `BRANCH-SWITCH-CONFIRMED` в response text. Controller пишет marker → OLD hook accepts → dangerous git ops proceed. **S8 closure НЕ активна** в migration window.

**Mitigation:** §8 Implementation order matrix явно: «Этап 2.3 MUST complete BEFORE Этап 3 enforce-mode activation». Альтернатива — Этап 3 включает регистрацию обновлённого branch-switch hook одной транзакцией.

---

## BYPASS-PARTIAL (17 findings — выборка топ-7)

### PARTIAL-1: Post-skill class-level unlock, не file-level

**Source:** Agent 1 / Finding 1.4 | **Confidence:** HIGH

§4.6 partial unlock + §4.5 answer parsing: `approved_tool: "Edit"` без mandatory `approved_action_pattern`. После `Skill(writing-plans)` + AskUser «выполнить изменения» controller может Edit **любой** non-protected файл. Class-level unlock = всё `Edit/Write/MultiEdit` разрешено.

**Fix:** `approved_action_pattern` mandatory с конкретным file path. Gate сравнивает `tool_input.file_path` против pattern.

### PARTIAL-2: Chain-reset через AskUser chosen_label

**Source:** Agent 1 / Finding 1.3 | **Confidence:** HIGH

Chain-state TTL expiry читает `transcript[-1].user.content` на explicit-reset фразы. Spec говорит «only user turns». Но AskUserQuestion chosen_label попадает в transcript **как user content** — controller помещает «новая задача» в option label, заказчик выбирает → chain reset triggered.

**Fix:** Reset фразы проверяются только против **organic root prompt**, не chosen labels. Или AskUser answers стрипают reset-phrase keywords.

### PARTIAL-3: Free-form answer → AskUser fatigue cascade

**Source:** Agent 1 / Finding 1.2 | **Confidence:** MEDIUM

Controller конструирует AskUser опции так, что заказчик пишет неоднозначное «давай» / «хорошо» в free-form. 2/2 limit → fail-CLOSE → cascade frustration → заказчик пишет «делай сам» → Поведение 1 trigger via word `делай`.

**Fix:** Strict explicit-keyword requirement в опциях. Чётко-классифицируемые answers (stop / specific tool / direct no skill).

### PARTIAL-4: TOCTOU double-consume askuser-decisions entry

**Source:** Agent 2 / Finding 3.1 | **Confidence:** HIGH

`enforce-branch-switch.mjs` и `enforce-router-gate.mjs` оба read-check-write на `consumed: false`. Proper-lockfile защищает write, но read-modify-write 3-шаговая операция не атомарна. Параллельные tools могут увидеть `consumed: false` оба → consume approval дважды.

**Fix:** Atomic compare-and-swap operation через single lock around full RMW. Lock acquired при read, released после write.

### PARTIAL-5: Chain_step++ двойной через sub-Skill

**Source:** Agent 2 / Finding 3.2 | **Confidence:** MEDIUM

PostToolUse handler инкрементирует chain_step на success. Если Skill(#55) внутри вызывает sub-Task с subagent_type matching expected_node → двойной инкремент.

**Fix:** PostToolUse handler сравнивает не только tool match, но и `expected_tool_use_id` который PreToolUse запомнил при разрешении. Increment только при exact tool_use_id match.

### PARTIAL-6: Edit-execute window между commit и watcher catch

**Source:** Agent 2 / Finding 3.4 | **Confidence:** HIGH

Если в turn N+1 первый tool — Bash(`node evil.mjs`), gate проверяет watcher (пуст после commit clear) → allow. Затем Edit(`evil.mjs`) в same turn → попадает в watcher. Но `node evil.mjs` уже выполнен.

**Fix:** PreToolUse Bash check: посмотреть **существование** target script + **mtime** ≤ 5 секунд → require AskUser regardless of watcher state.

### PARTIAL-7: chain-state malformed = fail-OPEN reset

**Source:** Agent 3 / Finding 6.1 | **Confidence:** HIGH

Asymmetric §3.3 failure modes: router-state malformed → block, chain-state malformed → reset + warn. Если controller тригернёт malformed (lock-contention read failure), chain progress wiped → Поведение 4 (один AskUser unlock all).

**Fix:** chain-state malformed → fail-CLOSE (block all mutating). Symmetric с router-state semantics.

**Остальные 10 BYPASS-PARTIAL** (краткий список):

- P-08 **ToolSearch deferred tools** (Agent 1 / F2.1) — gate matcher coverage unclear для loaded-on-demand tools
- P-09 **Skill execution gate coverage** (Agent 1 / F2.5) — нужен smoke test что tool calls в skill execution проходят через PreToolUse
- P-10 **`node.exe` variant + temp files** (Agent 1 / F5.1) — subagent creates non-protected helper.js, parent runs via `node.exe`
- P-11 **String concatenation static scan bypass** (Agent 1 / F5.4) — `const k = 'write' + 'File' + 'Sync'; fs[k](...)`
- P-12 **Parallel Task counter race** (Agent 2 / F3.3) — 3+ Tasks одновременно проходят check на 3-limit
- P-13 **Orphan cleanup 24h reset** (Agent 2 / F3.5) — long sessions могут exploit cleanup
- P-14 **`<!-- edited-files-clear -->` via AskUser social eng** (Agent 3 / F6.6) — controller suggests user paste marker
- P-15 **`<` input redirect not in blacklist** (Agent 3 / F7.6) — `wc -l < ~/.claude/runtime/state.json` обходит path-deny
- P-16 **Retry after timeout bypass** (Agent 2 / F8.1) — fail-CLOSE на timeout → retry next turn → fresh cache → success
- P-17 **TDD-gate vs router-gate execution order race** (Agent 3 / F9.1) — undefined hook ordering = TDD bypass через cascading unlock

---

## DOS Findings (9)

| ID | Finding | Severity | Mitigation |
|---|---|---|---|
| D-1 | `gate-config.json` writable migration window (Этап 1→2 gap) | HIGH — tiny budget = permanent fail-CLOSE | Hardcoded floor values; gate-config protected with Этапа 1.4 |
| D-2 | subagent block-file missing + tool_use_count==0 = assume BLOCKED → AskUser loop | HIGH — каждый Task triggers AskUser | Add success-marker file для clean subagent exits |
| D-3 | file-watcher reset stdout parsing fragile (FAIL string в commit message) | MEDIUM | Use lefthook exit code, не stdout strings |
| D-4 | Windows tmp+rename non-atomicity under AV load | MEDIUM | Wrap rename in retry+catch с logging |
| D-5 | Transcript parsing huge turn budget exhaustion | MEDIUM | Cap turn size при парсинге |
| D-6 | 10 tool calls lock contention cascade | MEDIUM | Reader-writer lock model |
| D-7 | Subagent gate reads parent locked state timeout | MEDIUM | Read-lock для parent state, не exclusive |
| D-8 | gate-config.json missing first start fail-CLOSE | MEDIUM | Explicit hardcoded defaults fallback |
| D-9 | enforce-prompt-injection empty surface drops context | MEDIUM | Verify hook handles empty array без crash |

---

## INFO / Acknowledged (15 findings)

Низкая feasibility или acknowledged design choice:

- TodoWrite context pollution (Agent 1 / F1.5)
- npm test DB side effects (Agent 1 / F2.3)
- Monitor timing attack (Agent 1 / F2.4)
- PostToolUse handler crash semantics (Agent 2 / F3.6)
- symlink swap TOCTOU (Agent 2 / F4.3) — требует mutating Bash который уже blocked
- Unicode homoglyph case-fold (Agent 2 / F4.5) — требует pre-existing homoglyph dir
- symlink + EACCES path normalization (Agent 1 / F5.2)
- git format-patch creates files in CWD (Agent 1 / F5.3) — covered by F4.7 deeper
- gate-config security posture impact (Agent 1 / F5.5)
- Read-only long reconnaissance leakage (Agent 3 / F6.2) — acknowledged Поведение 4
- `node --inspect` debugger port (Agent 3 / F7.2) — requires external debugger
- IFS manipulation (Agent 3 / F7.4) — `env`/`export` already blacklisted
- git format-patch --stdout info disclosure (Agent 3 / F7.5) — acknowledged read-only
- JSONL append > 4096 bytes atomicity (Agent 2 / F8.5) — log corruption only, не security
- first-option position bias defeats (Agent 3 / F9.5) — acknowledged residual

---

## Verdict on «hard wall полный» claim

**v3.5 spec заявление НЕ выдерживает.** Audit identified concrete BYPASS-COMPLETE vectors which 4 раунда внутренних audits упустили. Pattern причин:

1. **Bash content rules incomplete enumeration** — 6 critical bypasses в §5.1 single attack zone. Внутренние audits фокусировались на known bypass classes (`$()`/backtick/heredoc), пропустили `<<<` / `node` REPL semantics / `<` redirect / multi-arg path / `format-patch -o`.
2. **Windows-specific path normalization gaps** — UNC, 8.3 short names, env var expansion edge cases. Внутренние audits в основном на Unix-mental-model тестировались.
3. **Поведение 1 trigger source unrestricted** — `transcript[-1].user.content` включает AskUser chosen_label, что fundamentally breaks direct-invocation model. **Это самая серьёзная** — design-level flaw, не implementation detail.
4. **Implementation order matrix incomplete** — Этап 2.3 sequencing не enforced, migration window открывает S8 регрессию.

**Класс «controller-writable signals» НЕ полностью закрыт** — закрыты S5 (subagent BLOCKED) и S8 (branch-switch confirmation), но обнаружены **новые** controller-writable signals: AskUser option labels (CRITICAL-1), AskUser chosen_label контекст для chain reset (PARTIAL-2), AskUser content для social engineering (PARTIAL-3). Generalisable формула side-channel paths применима только к **explicit out-of-band channels** — но controller контролирует contents AskUserQuestion construction, что эффективно делает chosen_label semi-side-channel.

---

## Recommended actions (priority order)

**MUST FIX до writing-plans** (8 items):

1. **CRITICAL-9** — Add `<<<` к §5.1 sub-shell broad sweep (trivial fix, 1 строка)
2. **CRITICAL-8** — Add `node` без positional path arg → block (§5.1 hard-blacklist)
3. **CRITICAL-6** — Tokenizer per-arg path-deny check (§5.1 path-deny overlay)
4. **CRITICAL-1** — Поведение 1 detector ограничить только organic root prompt, не AskUser answers (§4 Поведение 1)
5. **CRITICAL-3** — Unresolved `$VAR` → fail-CLOSE (§3.1 step 3)
6. **CRITICAL-4** — Strip `\\?\` UNC prefix перед resolve (§3.1)
7. **CRITICAL-5** — Use `GetLongPathName` WinAPI для 8.3 expansion (§3.1)
8. **CRITICAL-10** — §8 matrix явно enforce Этап 2.3 BEFORE Этап 3

**SHOULD FIX в writing-plans** (5 items):

1. **CRITICAL-2** — Static scan §5.2 расширить на vitest config + setupFiles auto-discovery
2. **CRITICAL-7** — Verify `git format-patch -o` handling в parser implementation; explicit fix если per-command parser
3. **PARTIAL-1** — `approved_action_pattern` mandatory file-path binding (§4.5)
4. **PARTIAL-2** — Chain reset фразы только в organic root prompt (§3 chain-state)
5. **PARTIAL-7** — chain-state malformed → fail-CLOSE (§3.3 failure mode)

**NICE-TO-HAVE / D-* DOS fixes** в writing-plans:

- D-1 (gate-config protected с Этапа 1.4)
- D-2 (subagent success-marker file)
- D-3 (lefthook exit code не stdout parsing)
- D-9 (verify enforce-prompt-injection empty handling)

**ACCEPT as residual** (явно документировать в §9):

- 15 INFO findings — большинство acknowledged limitations или low feasibility
- PARTIAL-3 (AskUser fatigue) — UX-tradeoff
- PARTIAL-17 (TDD-gate ordering) — нуждается в smoke-test для verification

---

## Next steps

Заказчик решает:

**Option A — Patch condensed v3.5 inline:** добавить fixes для 8 MUST + 5 SHOULD в текущий condensed (commit поверх d4f7e681), пересохранить self-consistent v3.5.5. Spec становится 1117 → ~1250 строк.

**Option B — Open v3.6 round:** brainstorming-скил для design каждого fix отдельно (AskUserQuestion per fix), затем patch condensed. Более тщательно, +3-5h работы.

**Option C — Defer fixes в writing-plans:** записать audit findings в spec §9 как «known to be addressed in plan implementation», plan picks up specific resolution per finding. Faster but spec остаётся имеющим заявленные дыры.

**Option D — Hybrid:** A для 8 MUST (trivial fixes), C для 5 SHOULD (require design decisions в plan).

**Рекомендуемый:** D — закрыть 8 critical inline (час работы), оставить 5 SHOULD на plan для проектных решений per fix.

Audit complete.
