Hooks
What devpace runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
> /plugin marketplace add arch-team/devpace > /plugin install devpace@devpace
Ships with devpace. Installing the plugin gets these hooks.
What fires, and when
SessionStart
Fires once when a session begins, and again after a context compaction. It is where a plugin sets up its environment, or restores state the compaction dropped.
${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh
PreToolUse
- Matches
Write|Edit${CLAUDE_PLUGIN_ROOT}/hooks/pre-tool-use.mjs
PostToolUse
- Matches
Write|Edit${CLAUDE_PLUGIN_ROOT}/hooks/post-cr-update.mjs${CLAUDE_PLUGIN_ROOT}/hooks/pulse-counter.mjs${CLAUDE_PLUGIN_ROOT}/hooks/sync-push.mjs${CLAUDE_PLUGIN_ROOT}/hooks/post-schema-check.mjs
PostToolUseFailure
- Matches
Write|Edit${CLAUDE_PLUGIN_ROOT}/hooks/post-tool-failure.mjs
UserPromptSubmit
Fires before Claude sees each prompt you send. A plugin can use it to inject context, so the same instruction reaches the model every turn instead of only at session start.
${CLAUDE_PLUGIN_ROOT}/hooks/skill-eval.mjs
PreCompact
${CLAUDE_PLUGIN_ROOT}/hooks/pre-compact.sh
Stop
${CLAUDE_PLUGIN_ROOT}/hooks/session-stop.sh
SessionEnd
${CLAUDE_PLUGIN_ROOT}/hooks/session-end.sh
SubagentStop
${CLAUDE_PLUGIN_ROOT}/hooks/subagent-stop.mjs
Where it lives
- hooks/post-cr-update.mjsRunsGitHub
Read the script
#!/usr/bin/env node /** * devpace PostToolUse hook — detect CR state transitions and trigger pipelines * * Purpose: After a Write/Edit to a CR file, detect state transitions and output * appropriate ACTION signals: * - merged → post-merge pipeline (§11) * - created → auto-link/create suggestion (sync-procedures-auto.md) * - gate failures / rejections → learning triggers * * This is an advisory hook (exit 0), not blocking. */ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { basename, dirname } from 'node:path'; import { readStdinJson, getProjectDir, extractFilePath, isCrFile, readCrState, getLastEvent, CR_STATES } from './lib/utils.mjs'; const input = await readStdinJson(); const projectDir = getProjectDir(); const backlogDir = `${projectDir}/.devpace/backlog`; // Only act if .devpace exists and has backlog if (!existsSync(backlogDir)) { process.exit(0); } // Extract file path from tool input const filePath = extractFilePath(input); // Only check writes to CR files if (!isCrFile(filePath, backlogDir)) { process.exit(0); } // Check CR state and recent events for learning triggers if (existsSync(filePath)) { let content; try { content = readFileSync(filePath, 'utf-8'); } catch { process.exit(0); } const currentState = readCrState(filePath, content); const crName = basename(filePath, '.md'); if (currentState === CR_STATES.MERGED) { console.log(`devpace:post-merge ${crName} 已 merge。ACTION: 执行 post-merge 管线——1) 更新 state.md 移除该 CR 的进行中标记 2) 若 CR 有外部关联则执行 /pace-sync 同步状态到外部(关闭 Issue + 完成标签) 3) 执行 /pace-learn merge ${crName} 萃取经验。`); } // Auto-link/create trigger — new CR created with sync configured // Uses a marker file to avoid re-suggesting on subsequent edits to the same CR if (currentState === CR_STATES.CREATED) { const syncMappingPath = `${projectDir}/.devpace/integrations/sync-mapping.md`; const hasExternalLink = /\*\*外部关联\*\*[::]\s*\[/.test(content); const autoSuggestMark = `${projectDir}/.devpace/.sync-auto-suggested-${crName}`; if (existsSync(syncMappingPath) && !hasExternalLink && !existsSync(autoSuggestMark)) { try { mkdirSync(dirname(autoSuggestMark), { recursive: true }); writeFileSync(autoSuggestMark, new Date().toISOString(), 'utf-8'); } catch { /* non-critical */ } console.log(`devpace:auto-sync ${crName} 已创建且同步已配置。ACTION: 加载 sync-procedures-auto.md,为 ${crName} 执行自动关联/创建外部 Issue 流程(§2 auto-link 优先,无匹配则 §3 auto-create)。`); } } // Gate fail learning trigger — gate_fail is a valuable learning opportunity const recentEvent = getLastEvent(filePath, content); if (recentEvent && (recentEvent.type === 'gate1_fail' || recentEvent.type === 'gate2_fail')) { const gateNum = recentEvent.type === 'gate1_fail' ? '1' : '2'; console.log(`devpace:learn-trigger ${crName} Gate ${gateNum} 未通过。ACTION: 先修复 Gate 失败原因并重试;Gate 通过后执行 /pace-learn gate-failure ${crName} 萃取教训。`); } // Rejected learning trigger — human rejection reveals understanding gaps if (recentEvent && recentEvent.type === 'rejected') { console.log(`devpace:learn-trigger ${crName} 被人类驳回。ACTION: 查看 CR 事件表最新 rejected 记录确认驳回原因,修复后重新提交 review;完成后执行 /pace-learn rejection ${crName} 分析认知差距。`); } // Gate result sync trigger — push Gate results to external platform if (recentEvent && /^gate[123]_(pass|fail)$/.test(recentEvent.type)) { const syncMappingPath = `${projectDir}/.devpace/integrations/sync-mapping.md`; const hasExternalLink = /\*\*外部关联\*\*[::]\s*\[/.test(content); if (existsSync(syncMappingPath) && hasExternalLink) { const gateMatch = recentEvent.type.match(/^gate(\d)_(pass|fail)$/); const gateNum = gateMatch[1]; const gateResult = gateMatch[2] === 'pass' ? '通过' : '未通过'; console.log(`devpace:gate-sync ${crName} Gate ${gateNum} ${gateResult}。ACTION: 加载 sync-procedures-push-advanced.md §2,为 ${crName} 推送 Gate ${gateNum} 结果到外部平台(Comment + 标签)。`); } } } process.exit(0); - hooks/post-schema-check.mjsRunsGitHub
Read the script
#!/usr/bin/env node /** * PostToolUse hook — automatic Schema validation after .devpace/ file writes. * * Advisory only (exit 0) — outputs warnings/errors but never blocks. * Runs validate-schema.mjs on the written file if it's a validatable type * (CR, state, project, PF, BR). * * Exit codes: * 0 = always (advisory, non-blocking) */ import { existsSync } from 'node:fs'; import { basename, dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { execFileSync } from 'node:child_process'; import { readStdinJson, getProjectDir, extractFilePath, isDevpaceFile } from './lib/utils.mjs'; const input = await readStdinJson(); const projectDir = getProjectDir(); const filePath = extractFilePath(input); // Only check .devpace/ files if (!filePath || !isDevpaceFile(filePath)) { process.exit(0); } // Only check validatable file types const name = basename(filePath); const isValidatable = name === 'state.md' || name === 'project.md' || /^CR-\d{3}\.md$/.test(name) || /^PF-\d{3}\.md$/.test(name) || /^BR-\d{3}\.md$/.test(name); if (!isValidatable) { process.exit(0); } // Find .devpace directory from file path const devpaceMatch = filePath.match(/(.+\/.devpace)\//); if (!devpaceMatch) { process.exit(0); } const devpaceDir = devpaceMatch[1]; // Run validation try { const scriptDir = dirname(fileURLToPath(import.meta.url)); const scriptPath = join(scriptDir, '..', 'scripts', 'validate-schema.mjs'); if (!existsSync(scriptPath)) { process.exit(0); // Script not available, skip silently } let output; try { output = execFileSync( 'node', [scriptPath, devpaceDir, '--file', filePath], { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] } ); } catch (execErr) { // validate-schema.mjs exits 1 on errors — capture stdout from the error output = execErr.stdout || ''; } if (!output) { process.exit(0); } const result = JSON.parse(output); if (!result.valid) { const r = result.results[0]; const issues = [...r.errors.map(e => `error: ${e}`), ...r.warnings.map(w => `warning: ${w}`)]; const schemaPathMap = { 'state.md': 'process/state-format', 'project.md': 'entity/project-format', }; const schemaPath = schemaPathMap[name] || (name.startsWith('CR-') ? 'entity/cr-format' : name.startsWith('PF-') ? 'entity/pf-format' : name.startsWith('BR-') ? 'entity/br-format' : 'unknown'); console.log(`devpace:schema-check ${name} 校验不通过(${r.errors.length} error, ${r.warnings.length} warning):${issues.slice(0, 3).join('; ')}${issues.length > 3 ? ` (+${issues.length - 3} more)` : ''}. ACTION: 重新读取 ${name},按上述错误逐一修复,修复后重新写入触发再次校验。格式参考:knowledge/_schema/${schemaPath}-format.md。`); } } catch { // Validation failure is non-critical — skip silently } process.exit(0); - hooks/post-tool-failure.mjsRunsGitHub
Read the script
#!/usr/bin/env node /** * devpace PostToolUseFailure hook — detect tool failures in advance mode * * Purpose: When a Write/Edit tool fails during advance mode, remind Claude * to check CR state consistency. Prevents CR state and file content from * becoming out of sync after a failed write operation. * * Advisory hook (exit 0), not blocking. */ import { existsSync } from 'node:fs'; import { readStdinJson, getProjectDir, extractFilePath, isCrFile, isAdvanceMode } from './lib/utils.mjs'; const input = await readStdinJson(); const projectDir = getProjectDir(); const backlogDir = `${projectDir}/.devpace/backlog`; // Only act if .devpace exists and we're in advance mode if (!existsSync(backlogDir) || !isAdvanceMode(projectDir)) { process.exit(0); } const filePath = extractFilePath(input); // Check if the failed write was targeting a CR file if (isCrFile(filePath, backlogDir)) { console.log(`devpace:tool-failure CR 文件写入失败。ACTION: 1) 读取 CR 文件确认状态字段是否仍为上次成功值 2) 若状态不一致则在事件表补记 write_failed 条目 3) 执行 git diff ${filePath} 检查部分写入,必要时 git checkout -- ${filePath} 恢复。`); } else if (filePath && filePath.includes('.devpace/')) { console.log(`devpace:tool-failure .devpace/ 文件写入失败。ACTION: 读取 state.md 确认与当前进度一致;若不一致则执行 git checkout -- ${filePath} 恢复后重试写入。`); } process.exit(0); - hooks/pre-compact.shRunsGitHub
Read the script
#!/bin/bash # devpace PreCompact hook — save state snapshot and inject recovery context # # Purpose: Before Claude auto-compacts (context ~95%), output structured # recovery context that will survive compression, ensuring continuity. # Enhances cross-session continuity (OBJ-1) and long-session stability (UX4). # # Advisory hook (exit 0), not blocking. PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}" STATE_FILE="${PROJECT_DIR}/.devpace/state.md" DEVPACE_DIR="${PROJECT_DIR}/.devpace" if [ ! -f "$STATE_FILE" ]; then exit 0 fi # Extract key context for post-compact recovery echo "devpace:pre-compact === DEVPACE RECOVERY CONTEXT (preserve after compact) ===" # 1. Iron Rules reminder echo "devpace:pre-compact IR-1: state.md 是唯一会话锚点 | IR-2: 先读后写 | IR-3: 质量门不可跳过 | IR-4: 变更走流程 | IR-5: 产出可追溯" # 2. Current state summary (extract from state.md) if [ -f "$STATE_FILE" ]; then # Extract 进行中 and 下一步 from state.md CURRENT=$(grep -m1 "进行中\|developing\|verifying\|in_review" "$STATE_FILE" 2>/dev/null || echo "") NEXT_STEP=$(grep -m1 "下一步" "$STATE_FILE" 2>/dev/null || echo "") if [ -n "$CURRENT" ]; then echo "devpace:pre-compact Current: $CURRENT" fi if [ -n "$NEXT_STEP" ]; then echo "devpace:pre-compact Next: $NEXT_STEP" fi fi # 3. Active CR detection + execution snapshot extraction if [ -d "${DEVPACE_DIR}/backlog" ]; then ACTIVE_CRS=$(grep -rl "developing\|verifying\|in_review" "${DEVPACE_DIR}/backlog/" 2>/dev/null | head -3) if [ -n "$ACTIVE_CRS" ]; then for cr in $ACTIVE_CRS; do [ ! -f "$cr" ] && continue CR_NAME=$(basename "$cr" .md) CR_STATUS=$(grep -m1 "状态" "$cr" 2>/dev/null | head -1) echo "devpace:pre-compact Active CR: $CR_NAME — $CR_STATUS" # Extract execution snapshot restore hint (L/XL CRs only) snapshot=$(sed -n '/^## 执行快照/,/^## /p' "$cr" | head -10) if [ -n "$snapshot" ]; then restore_hint=$(echo "$snapshot" | grep '恢复建议' | sed 's/.*| //') if [ -n "$restore_hint" ]; then echo "devpace:pre-compact 快照: $restore_hint" fi fi done fi fi echo "devpace:pre-compact ACTION: 1) Read .devpace/state.md to restore full context 2) Resume active CR 3) Git commit any uncommitted changes" echo "devpace:pre-compact === END RECOVERY CONTEXT ===" exit 0 - hooks/pre-tool-use.mjsRunsGitHub
Read the script
#!/usr/bin/env node /** * devpace PreToolUse hook — enforcing quality gates and mode constraints * * Purpose: Enforce devpace iron rules at the mechanism level, not just text-based rules. * * Enforcement levels: * 1. BLOCKING (exit 2): Explore mode state escalation, Gate 3 bypass attempts * 2. ADVISORY (exit 0): Gate 1/2 reminders during normal development flow * * Iron rules enforced: * - Explore mode: block state.md writes and CR state escalation to advance-mode * states (developing/verifying/in_review). Allow other .devpace/ writes so * management Skills (pace-change, pace-biz, pace-plan) can operate. * - Gate 3: human approval required, no automated state change to approved (devpace-rules.md §2) */ import { existsSync } from 'node:fs'; import { readStdinJson, getProjectDir, extractFilePath, extractWriteContent, isCrFile, readCrState, isDevpaceFile, isAdvanceMode, isStateChangeToApproved, isStateEscalation, CR_STATES } from './lib/utils.mjs'; const input = await readStdinJson(); const projectDir = getProjectDir(); const backlogDir = `${projectDir}/.devpace/backlog`; // Only act if .devpace exists and has backlog if (!existsSync(backlogDir)) { process.exit(0); } const filePath = extractFilePath(input); // ── ENFORCEMENT 1: Explore mode protection ────────────────────────── // Narrowed scope: only block high-risk state operations in explore mode. // Management Skills (pace-change, pace-biz, pace-plan) need to write to // .devpace/ files even without an active CR, so we only block: // 1. state.md direct modification — progress state shouldn't change in explore mode // 2. CR state escalation — setting developing/verifying/in_review requires advance mode if (isDevpaceFile(filePath) && !isAdvanceMode(projectDir)) { const isStateMd = filePath.endsWith('/state.md') || filePath.endsWith('/.devpace/state.md'); const isCrStateEsc = isCrFile(filePath, backlogDir) && isStateEscalation(extractWriteContent(input)); if (isStateMd || isCrStateEsc) { console.error('devpace:blocked 探索模式禁止修改进度状态(state.md 或 CR 状态升级)。ACTION: 告知用户需要先进入推进模式,引导用户说"帮我实现 X"或"开始做 CR-NNN"以激活 /pace-dev。'); process.exit(2); } } // ── ENFORCEMENT 2 + ADVISORY: Gate checks ─────────────────────────── // Single read for both Gate 3 enforcement and advisory reminders if (isCrFile(filePath, backlogDir) && existsSync(filePath)) { const currentState = readCrState(filePath); // Gate 3: human approval required — in_review → approved blocked if (currentState === CR_STATES.IN_REVIEW) { const newContent = extractWriteContent(input); if (isStateChangeToApproved(newContent)) { console.error('devpace:blocked Gate 3 铁律:CR 从 in_review→approved 必须由人类明确批准。ACTION: 向用户展示 review 摘要(diff 概要+验收标准对比),然后询问是否批准该变更,等待用户回复批准/approved后再修改状态。'); process.exit(2); } } // Advisory: Quality gate reminders switch (currentState) { case CR_STATES.DEVELOPING: console.log("devpace:gate-reminder CR 状态 developing。推进到 verifying 前须通过 Gate 1。ACTION: 执行 lint+test+typecheck,全部通过后在 CR 事件表记录 gate1_pass,再将状态改为 verifying。"); break; case CR_STATES.VERIFYING: console.log("devpace:gate-reminder CR 状态 verifying。推进到 in_review 前须通过 Gate 2。ACTION: 执行集成测试+意图一致性检查(对比 CR 验收标准与实际实现),通过后在事件表记录 gate2_pass,再将状态改为 in_review。"); break; case CR_STATES.IN_REVIEW: console.log("devpace:gate-reminder CR 状态 in_review。Gate 3 须人类批准。ACTION: 向用户展示变更摘要(diff 概要+验收标准对比),等待用户明确说'批准'。"); break; } } process.exit(0); - hooks/pulse-counter.mjsRunsGitHub
Read the script
#!/usr/bin/env node /** * devpace PostToolUse hook — periodic write-count reminder * * Purpose: Track write operations and periodically remind Claude to check * project status. This is a WRITE VOLUME reminder, complementary to but * distinct from pace-pulse (rhythm health detection). * * Coordination with pace-pulse: * - pulse-counter: triggers every 10 writes → suggests /pace-status (write volume) * - pace-pulse: triggers every 5 checkpoints or 30min → detects rhythm anomalies * - If pace-pulse ran recently (< 5 min), this hook skips its reminder to avoid * double-reminding the user in a short window. * * Uses .devpace/.pulse-counter as persistent counter (not version-controlled). * Uses .devpace/.pulse-last-run as pace-pulse timestamp (written by advance mode). * * This is an advisory hook (exit 0), never blocks. */ import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { basename } from 'node:path'; import { readStdinJson, getProjectDir, extractFilePath, isCrFile, readCrState } from './lib/utils.mjs'; const input = await readStdinJson(); const projectDir = getProjectDir(); const devpaceDir = `${projectDir}/.devpace`; const counterFile = `${devpaceDir}/.pulse-counter`; const pulseLastRunFile = `${devpaceDir}/.pulse-last-run`; // Only act if .devpace exists if (!existsSync(devpaceDir)) { process.exit(0); } // Read and increment counter let count = 0; try { const raw = readFileSync(counterFile, 'utf-8').trim(); count = parseInt(raw, 10) || 0; } catch { // Counter file doesn't exist yet — start at 0 } count += 1; // Write updated counter try { writeFileSync(counterFile, String(count), 'utf-8'); } catch { // Can't write counter — degrade silently process.exit(0); } // --- Stuck detection: same CR written 5+ times without state change --- // Throttled to every 3rd write to reduce per-write I/O (reads JSON + CR file + writes JSON). const filePath = extractFilePath(input); const backlogDir = `${devpaceDir}/backlog`; if (isCrFile(filePath, backlogDir) && count % 3 === 0) { const crWritesPath = `${devpaceDir}/.pulse-cr-writes`; let writes = {}; try { writes = JSON.parse(readFileSync(crWritesPath, 'utf-8')); } catch { /* start fresh */ } const crName = basename(filePath, '.md'); const currentState = readCrState(filePath); if (!writes[crName] || writes[crName].last_state !== currentState) { writes[crName] = { count: 1, last_state: currentState }; } else { writes[crName].count++; } // Prune stale entries — keep only CRs still in backlog (max 20 as safety cap) const keys = Object.keys(writes); if (keys.length > 20) { for (const k of keys) { if (!existsSync(`${backlogDir}/${k}.md`)) { delete writes[k]; } } } try { writeFileSync(crWritesPath, JSON.stringify(writes), 'utf-8'); } catch { /* silent */ } if (writes[crName].count >= 5) { console.log(`devpace:stuck-warning ${crName} 已被写入 ${writes[crName].count} 次但状态仍为 ${currentState},可能在空转。ACTION: 执行 /pace-status 检查是否有阻塞项;若有则解决阻塞后继续;若无则审视当前方案——考虑 /pace-change 调整范围或分拆 CR。`); console.log(`devpace:struggle-signal ${crName} 重复写入可能指示环境缺陷(Skill/procedure/Schema 不足)。ACTION: 当前继续完成 CR 任务;CR merged 后执行 /pace-learn 萃取改进建议。`); } } // Trigger write-volume reminder every 10 writes if (count > 0 && count % 10 === 0) { // Check if pace-pulse ran recently (< 5 min) — skip if so to avoid double-remind let skipReminder = false; try { const lastRunStr = readFileSync(pulseLastRunFile, 'utf-8').trim(); const lastRunMs = parseInt(lastRunStr, 10) || 0; if (lastRunMs > 0 && (Date.now() - lastRunMs) < 5 * 60 * 1000) { skipReminder = true; } } catch { // No pulse-last-run file — pace-pulse hasn't run, proceed normally } if (!skipReminder) { console.log(`devpace:write-volume 已执行 ${count} 次写操作。ACTION: 执行 /pace-status 查看项目进度,确认当前工作节奏正常。`); } } process.exit(0); - hooks/session-end.shRunsGitHub
- hooks/session-start.shRunsGitHub
- hooks/session-stop.shRunsGitHub
- hooks/skill-eval.mjsRunsGitHub
- hooks/subagent-stop.mjsRunsGitHub
- hooks/sync-push.mjsRunsGitHub
All 12 scripts are listed above. The source is inlined for 6 of them, starting with whatever hooks.json actually runs. See all of them in the repo.
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Give your Claude Code projects a steady development pace — requirements change, rhythm stays. A development harness for Claude Code — rules, schemas, gates, and feedback loops that keep AI-assisted development traceable and measurable.
Repo: arch-team/devpace

