1. 背景与问题
前端工程化发展至今,脚手架、构建、测试、CI/CD 已经相当成熟,但在「Code Agent 时代」出现了一个新的工程化课题:如何把AI 编码工具(Claude Code)接入既有前端工程,让它既能安全地写代码,又能自动化完成测试、修复、评审,形成可复用的Harness(工程化夹具/驱动骨架)。
传统前端项目里,「Harness」通常指测试夹具(Test Harness)或运行环境封装层,用来隔离被测系统并注入依赖。本文把 Harness 的概念向前推进一步:为 Claude Code 构建一个可执行、可观测、可约束的工程化运行环境,让 AI 在本地仓库中完成「改代码 → 跑测试 → 修问题 → 出报告」的闭环。
本文会解决以下问题:
- Claude Code 如何通过CLI 无头模式接入 Node 脚本,而非手动对话。
- 如何用自定义 Slash Command沉淀团队工作流。
- 如何用Hooks做质量门禁和危险操作拦截。
- 如何把前端的lint / type-check / test接入反馈闭环。
- 如何搭建一个可复用的 Harness 工程,一键驱动整套流程。
说明:本文以 Claude Code(
claudeCLI,Anthropic 官方命令行编码工具)为例,所有方案同样可抽象到其他支持 CLI / MCP 的 Code Agent。
2. 整体架构设计
我们把方案拆为四层,核心思想是Harness 作为稳定的执行外壳,Claude Code 作为会写代码的执行单元:
- Harness CLI:负责准备环境、拼装 prompt、调用
claude、解析结果、跑质量门禁、决定是否重试。 - Claude Code Headless:
claude -p无头模式,可被 Node/Shell 驱动。 - 自定义命令 / Subagent / Skill:把团队规范固化为可复用资产。
- 质量门禁:AI 改完代码后,Harness 立即执行校验,失败则把错误信息回灌给 Claude Code 继续修复。
3. 环境准备
3.1 前置依赖
node-v# >= 18npm-v# >= 9claude--version# 需先安装 Claude Code CLI安装 Claude Code(如未安装):
npminstall-g@anthropic-ai/claude-code claude# 首次运行会引导登录 Anthropic 账号3.2 项目结构
我们以一个 Vue 3 + TypeScript + Vite 项目为例,Harness 单独放在.harness/目录,与业务代码解耦:
frontend-harness-demo/ ├── .claude/ │ ├── settings.json # Claude Code 项目级配置 │ ├── commands/ # 自定义 Slash Commands │ │ ├── fix-lint.md │ │ └── review.md │ └── skills/ # 技能包 │ └── fe-standard/ │ └── SKILL.md ├── .harness/ │ ├── cli.mjs # Harness 主入口 │ ├── prompts.mjs # Prompt 模板 │ ├── gates.mjs # 质量门禁 │ └── report.mjs # 报告生成 ├── src/ │ └── ... # 业务代码 ├── package.json ├── vitest.config.ts └── tsconfig.json4. 实战一:Claude Code Headless 最小闭环
Claude Code 提供无头模式-p / --print,可以在非交互环境下执行一次 prompt 并输出纯文本,非常适合被脚本包裹。
先写一个最简单的 Harness:
// .harness/cli.mjsimport{execSync}from'node:child_process';consttask=process.argv.slice(2).join(' ');if(!task){console.error('Usage: node .harness/cli.mjs "<任务描述>"');process.exit(1);}constcommand=`claude -p "${task}" --output-format json`;try{constraw=execSync(command,{encoding:'utf8',maxBuffer:1024*1024*10,});constresult=JSON.parse(raw);console.log('=== Claude Code 输出 ===');console.log(result.result);}catch(err){console.error('Claude Code 执行失败:',err.message);process.exit(1);}执行:
node.harness/cli.mjs"在 src/utils 下新增 formatDate.ts,把时间戳格式化为 YYYY-MM-DD HH:mm:ss"这样就完成了「脚本驱动 AI 写代码」的第一步。但直接裸调存在几个问题:
- 缺少上下文:Claude Code 需要明确的仓库背景与约束。
- 缺少验证:改完没有自动跑测试。
- 缺少重试:一次失败就退出,无法形成闭环。
下面逐层补齐。
5. 实战二:Prompt 模板与上下文注入
把易变的「任务」和稳定的「工程约束」分离,是 Harness 化的关键。我们用一个prompts.mjs负责拼接:
// .harness/prompts.mjsimportfsfrom'node:fs';importpathfrom'node:path';constPROJECT_ROOT=process.cwd();exportfunctionbuildTaskPrompt(task){constmdFiles=collectMarkdown(PROJECT_ROOT,['node_modules','.git','dist']);return['你是一名资深前端工程师,请在当前仓库中完成以下任务。','','## 工程约束(必须遵守)','- 使用 TypeScript,禁止 any,除非有充分理由并加注释。','- 遵循项目已有的代码风格,新增文件需要包含清晰的 JSDoc。','- 组件一律用 Vue 3 Composition API + `<script setup lang="ts">`。','- 不要修改 package.json 中与任务无关的依赖。','- 修改完成后,列出所有改动文件及原因。','','## 背景资料(节选)',...mdFiles.slice(0,8).map((f)=>`###${f.path}\n${f.content}`),'','## 本次任务',task,].join('\n');}functioncollectMarkdown(root,ignores){constresults=[];constwalk=(dir)=>{for(constnameoffs.readdirSync(dir)){if(ignores.includes(name))continue;constfull=path.join(dir,name);conststat=fs.statSync(full);if(stat.isDirectory()){walk(full);}elseif(name.endsWith('.md')){results.push({path:path.relative(root,full),content:fs.readFileSync(full,'utf8').slice(0,1200),});}}};walk(root);returnresults;}然后在主入口接入:
// .harness/cli.mjs 追加import{buildTaskPrompt}from'./prompts.mjs';consttask=process.argv.slice(2).join(' ');constprompt=buildTaskPrompt(task);execSync(`claude -p${shellQuote(prompt)}--output-format json`,{encoding:'utf8',stdio:['ignore','pipe','inherit'],});注意:直接拼 shell 字符串有注入风险,生产环境建议改用临时文件 +
claude -p "$(cat prompt.txt)",或通过spawn数组参数传递。
import{spawnSync}from'node:child_process';functionrunClaude(prompt){constresult=spawnSync('claude',['-p',prompt,'--output-format','json'],{encoding:'utf8',maxBuffer:1024*1024*20,});if(result.status!==0){thrownewError(`claude exited with${result.status}:${result.stderr}`);}returnJSON.parse(result.stdout);}6. 实战三:质量门禁与自动修复闭环
这是 Harness 的核心价值:AI 改完必须经过门禁,失败则把错误回灌,循环修复。
6.1 门禁定义
// .harness/gates.mjsimport{spawnSync}from'node:child_process';constGATES=[{name:'type-check',command:'npm',args:['run','type-check']},{name:'lint',command:'npm',args:['run','lint']},{name:'unit-test',command:'npm',args:['run','test:unit','--','--run']},];exportfunctionrunGates(){constpassed=[];constfailed=[];for(constgateofGATES){constresult=spawnSync(gate.command,gate.args,{encoding:'utf8',maxBuffer:1024*1024*10,});if(result.status===0){passed.push(gate.name);}else{failed.push({name:gate.name,output:result.stdout+result.stderr});}}return{passed,failed};}6.2 闭环主流程
// .harness/cli.mjs 完整闭环import{runGates}from'./gates.mjs';import{buildTaskPrompt,buildFixPrompt}from'./prompts.mjs';asyncfunctionmain(){consttask=process.argv.slice(2).join(' ');constMAX_ROUNDS=3;// 第一轮:执行任务letprompt=buildTaskPrompt(task);letresult=runClaude(prompt);console.log('[harness] 任务执行完成,进入门禁校验');// 循环修复for(letround=1;round<=MAX_ROUNDS;round++){const{passed,failed}=runGates();if(failed.length===0){console.log(`[harness] 全部门禁通过 ->${passed.join(', ')}`);return;}console.log(`[harness] 第${round}轮门禁失败:${failed.map((f)=>f.name).join(', ')}`);constfixPrompt=buildFixPrompt(failed);result=runClaude(fixPrompt);}console.error('[harness] 达到最大修复轮数,仍有门禁未通过');process.exit(1);}main();6.3 修复 Prompt
// .harness/prompts.mjs 追加exportfunctionbuildFixPrompt(failed){constdetails=failed.map((f)=>`###${f.name}失败输出\n\`\`\`\n${f.output.slice(0,4000)}\n\`\`\``).join('\n\n');return['上一轮修改未通过质量门禁,请阅读下面的错误输出,定位并修复问题。','只修改与失败相关的代码,不要重构无关逻辑。','',details,'','修复后请确认:类型检查通过、lint 无误、单元测试通过。',].join('\n');}package.json 中对应的脚本:
{"scripts":{"type-check":"vue-tsc --noEmit","lint":"eslint . --ext .ts,.vue","test:unit":"vitest"}}7. 实战四:自定义 Slash Commands 沉淀工作流
让团队每个人都手写冗长 prompt 不现实,Claude Code 支持把常用流程沉淀为.claude/commands/*.md,在会话里用/命令名调用。
7.1 代码评审命令
<!-- .claude/commands/review.md --> 请对当前改动(git diff 相对 HEAD)做严格代码评审,重点检查: 1. **正确性**:逻辑边界、空值、异步竞态、内存泄漏。 2. **类型安全**:是否存在 any、类型断言滥用、可空值未处理。 3. **可维护性**:命名语义、函数职责单一、重复代码。 4. **前端专项**: - 组件副作用是否在 onUnmounted 清理; - 是否存在不必要的响应式依赖; - 样式是否破坏响应式布局; - 可访问性(语义标签、键盘导航、焦点管理)。 输出格式: - 按严重程度分级:🔴 阻塞 / 🟡 建议 / 🟢 微优化。 - 每条问题给出文件位置、问题描述、修复建议代码片段。 - 最后给出「是否可以合并」的结论。 只评审本次改动,不要修改代码。7.2 修复 Lint 命令
<!-- .claude/commands/fix-lint.md --> 请运行 `npm run lint`,并修复所有可自动修复的问题。 约束: - 优先使用 `npx eslint . --fix`; - 对无法自动修复的问题,逐个分析并手工修正; - 不改变业务逻辑,不升级依赖; - 修复完成后再次运行 lint 确认 0 错误。7.3 生成测试命令
<!-- .claude/commands/gen-test.md --> 请为当前未覆盖的核心模块生成 Vitest 单元测试。 要求: - 使用 Vitest + @vue/test-utils,依赖与项目保持一致; - 覆盖正常路径、边界值、异常分支; - 测试命名使用「应该…」可读风格; - 不修改被测源码,除非发现明显 bug 需要先报告。 生成后运行 `npm run test:unit -- --run` 确认通过。这样,开发者在 Claude Code 交互会话里输入/review、/fix-lint、/gen-test即可复用团队规范。
8. 实战五:Hooks 做危险操作拦截与自动校验
Claude Code 的 Hooks 允许在 AI 执行特定操作前后插入自定义脚本,适合做「安全护栏」。配置位于.claude/settings.json。
8.1 项目级配置
{"permissions":{"allow":["Bash(npm run type-check:*)","Bash(npm run lint:*)","Bash(npm run test:unit:*)","Read(~/.harness/**)"],"deny":["Bash(git push:*)","Bash(rm -rf:*)","Bash(git reset --hard:*)","Edit(.env:*)","Edit(.npmrc:*)"]},"hooks":{"PostToolUse":[{"matcher":"Edit|Write","hooks":[{"type":"command","command":"bash .claude/hooks/auto-format.sh"}]}],"PreToolUse":[{"matcher":"Bash","hooks":[{"type":"command","command":"bash .claude/hooks/guard-bash.sh"}]}]}}关键点:
permissions.deny直接拦截git push、rm -rf、git reset --hard等危险命令,即使 AI 想执行也会被 CLI 权限层拒绝。PostToolUse在每次写文件后触发自动格式化。PreToolUse在执行任意 Bash 前做二次校验。
8.2 自动格式化 Hook
#!/usr/bin/env bash# .claude/hooks/auto-format.shset-euopipefail# 读取 stdin 中的 JSON 工具输入INPUT=$(cat)# 示例:仅对 .vue/.ts 文件触发 prettierFILE=$(echo"$INPUT"|jq-r'.tool_input.file_path // empty')if[[-n"$FILE"&&"$FILE"=~\.(ts|vue|js)$]];thennpx prettier--write"$FILE">/dev/null2>&1||truefiecho"$INPUT"8.3 危险命令守卫 Hook
#!/usr/bin/env bash# .claude/hooks/guard-bash.shset-euopipefailINPUT=$(cat)COMMAND=$(echo"$INPUT"|jq-r'.tool_input.command // ""')DANGEROUS_PATTERNS=('git push''rm -rf''git reset --hard''sudo ''docker rm -f')forpatternin"${DANGEROUS_PATTERNS[@]}";doif[["$COMMAND"==*"$pattern"*]];thenecho'{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"该命令被 Harness 安全策略拦截,请人工确认。"}}'exit2fidone# 允许执行echo'{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecis