TASK-0106 / Issue #289 対応。EH-3 (
scripts/hooks/check-plan-hash.sh) の TASK 非依存 Edit/Write を 人間が in-session で許可する一級手段。 AI 自己付与は 多層 best-effort 防御 + 全試行監査ログ で抑止する (完全な構造保証は別 PBI 分割・R-012)。
PlanGate の EH-3 は「TASK 文脈を持たない non-plan ファイルの Edit/Write」を ブロックする承認境界実行正本。人間が小規模な docs 整合修正等を行うとき、 この境界をセッション再起動なしで一時的に開く手段。
plangate maintenance start --reason "<text>" [--paths <glob,..>] [--minutes 1..30] [--force]人間が対話 TTY で実行。以下 4 層 best-effort 防御を順次チェック:
| 層 | チェック | reject 条件 |
|---|---|---|
| L1 | isatty(stdin) |
非対話実行 (パイプ/CI 等) |
| L2 | env barrier | CI / CLAUDE_AGENT / CURSOR_AGENT / PLANGATE_BYPASS_HOOK が set |
| L3 | parent process heuristic | ppid の comm 名が claude/codex/cursor を含む(ps -p $PPID -o comm= + grep -iqE) |
| L4 | 対話 nonce | 画面表示の 8-hex を PLANGATE_MAINT_ACK> プロンプトで再入力、一致しないと reject |
すべての試行(成功・失敗問わず)は docs/working/_audit/hook-events.log
に append される:
{"ts":"2026-05-21T...Z","event":"maintenance_start_attempt","verdict":"ok|reject_L1|reject_L2|reject_L3|reject_L4|reject_arg|reject_existing","ppid":12345,"isatty_stdin":true,"detail":"..."}
| Flag | Default | 説明 |
|---|---|---|
--reason |
(required) | 空白のみは reject |
--paths |
(empty) | カンマ区切り glob (例 "README.md,docs/**/*.md")。未指定なら Override 対象以外を許可(v1 後方互換) |
--minutes |
5 |
TTL 1..30 (hard cap 30) |
--force |
(off) | 既存有効窓がある場合に上書き許可 (AC-9) |
# 単発で README.md だけ 10 分許可
plangate maintenance start --reason "fix Provider table typo" --paths "README.md" --minutes 10
# docs ツリー全般 5 分
plangate maintenance start --reason "Changelog sync" --paths "docs/**/*.md"
# 既定 (paths 無指定 = Override 対象以外を許可) で 3 分
plangate maintenance start --reason "small docs fix" --minutes 3
plangate maintenance stop維持窓を即時失効(maintenance.json を削除)。
plangate maintenance helpusage を表示。
maintenance.json 存在時、EH-3 は以下の順序で判定:
(i) target_file 正規化 (./ 除去・絶対パス → repo 相対) [R-028]
(ii) Hardening Override 物理先頭判定 (10 パターン → block) [R-003/R-015]
(iii) maintenance ファイル base validation (TTL 内/approved_by 等)
(iv) allowed_paths スコープ判定 (未指定なら Override 以外を許可) [R-004]
(v) one_shot=true なら flock(LOCK_EX|LOCK_NB) → fstat vs stat で
inode 比較 → 再 read で consumed_at 未消費確認 → os.replace で
atomic 書込 → 解放 → SKIP [R-002/R-017/R-027/R-031]
競合検出/inode 不一致は fail-closed (block)
「防御の防御」: maintenance 窓内でも以下は変更不可。重要 infra への変更は 別承認ルート(PR + Human merge)必須。
.claude/rules/*.md.claude/settings.json / .claude/settings.local.json / .claude/settings.example.json.claude/commands/**.md.claude/agents/**.mdscripts/hooks/*.shbin/plangateschemas/*.schema.json.github/workflows/*.yml / *.yamlAGENTS.md / CLAUDE.mdplangate doctor # 末尾に「Maintenance Window」セクション (1 行)
plangate doctor --json --scope maintenance # 機械可読 metadata
JSON 出力例 (active):
{
"scope": "maintenance",
"checks": [{"id":"maintenance.json","level":"info","status":"ok","msg":"active: remaining=04:32 scope=test paths=['README.md']"}],
"failures": 0,
"warnings": 0,
"passed": true,
"maintenance": {
"present": true,
"scope": "test",
"approved_by": "mine_take",
"reason": "small docs fix",
"granted_at": 1779337000,
"until_epoch": 1779337300,
"remaining_seconds": 200,
"remaining_mmss": "03:20",
"allowed_paths": ["README.md"],
"one_shot": true,
"consumed_at": null,
"active": true
}
}
grep -iqE 'claude|codex|cursor' 部分一致。
ppid 偽装やエージェント名の変更には追従不可。docs/working/_audit/hook-events.log に全 start 試行が記録される。AI
偽装試行を検知するには:
# 失敗試行のみ抽出
grep -E '"verdict":"reject_' docs/working/_audit/hook-events.log
# 過去 1 時間の reject 集計
python3 -c "
import json, time
cutoff = time.time() - 3600
for line in open('docs/working/_audit/hook-events.log'):
line = line.strip()
if not line.startswith('{'): continue
try: d = json.loads(line)
except: continue
if d.get('event','').startswith('maintenance_start') and d.get('verdict','').startswith('reject'):
print(d['ts'], d['verdict'], d.get('detail',''))
"
docs/working/TASK-0106/).claude/rules/responsibility-classes.md 「対外公開アーティファクト
publish 責務分界」節 (関連: R-012 best-effort + 別 PBI 分割の方針)scripts/hooks/check-plan-hash.shschemas/maintenance.schema.json (v2)