@su-record/vibe 2.9.20 → 2.9.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -69,8 +69,9 @@ No `console.log` in commits · No hardcoded strings/numbers · No commented-out
69
69
  `/vibe.spec` is the single entry point — orchestrates interview → plan → spec → review → `/vibe.run` → `/vibe.verify` → `/vibe.contract` → `/vibe.trace`. For UI types (website/webapp/mobile), `/vibe.figma` branches in parallel. Smart Resume detects existing `.claude/vibe/{interviews,plans,specs}/*.md` to skip phases.
70
70
 
71
71
  **Quality-loop commands** (bug → prevention):
72
- - `/vibe.regress` — 회귀 테스트 자동 진화. `/vibe.verify` 실패 시 자동 register, `generate`로 예방 테스트 생성, `cluster`로 반복 패턴 승격.
73
- - `/vibe.contract` — API 계약 드리프트 감지. SPEC에서 추출한 계약과 구현 비교, P1 drift `/vibe.regress`로 자동 전파.
72
+ - `/vibe.regress` — Regression test auto-evolution. Auto-registers on `/vibe.verify` failure; `generate` produces preventive tests; `cluster` promotes recurring patterns.
73
+ - `/vibe.contract` — API contract drift detection. Compares the contract extracted from the SPEC against the implementation; P1 drift auto-propagates to `/vibe.regress`.
74
+ - `/vibe.test` — vibe self-test across the CC ↔ coco harnesses. Subcommands: `parity` (static), `report` (runtime), `compare` (diff). P1 drift auto-propagates to `/vibe.regress`. Recommended before every release.
74
75
 
75
76
  | Task Size | Approach |
76
77
  |---|---|
@@ -98,7 +99,7 @@ No `console.log` in commits · No hardcoded strings/numbers · No commented-out
98
99
 
99
100
  ## Git
100
101
 
101
- **Include**: `.claude/vibe/{plans,specs,features,todos,research,regressions,contracts}/`, `.claude/vibe/config.json`, `CLAUDE.md`
102
+ **Include**: `.claude/vibe/{plans,specs,features,todos,research,regressions,contracts,test-reports}/`, `.claude/vibe/config.json`, `CLAUDE.md`
102
103
  **Exclude**: `~/.claude/{rules,commands,agents,skills}/`, `.claude/settings.local.json`
103
104
 
104
105
  <!-- VIBE:END -->
@@ -5,57 +5,57 @@ argument-hint: "extract | check | diff [feature-name]"
5
5
 
6
6
  # /vibe.contract
7
7
 
8
- **API Contract Drift Detection** — SPEC에 적힌 API 계약과 실제 구현이 어긋나면 즉시 잡는다.
8
+ **API Contract Drift Detection** — when implementation diverges from the SPEC's API contract, catch it immediately.
9
9
 
10
- > SPEC 진실의 원천이다. 구현이 SPEC 소리 없이 떠나면 테스트는 통과해도 계약은 깨진다.
10
+ > The SPEC is the source of truth. If the implementation silently leaves the SPEC, tests can pass while the contract breaks.
11
11
 
12
12
  ## Usage
13
13
 
14
14
  ```
15
- /vibe.contract extract <feature> # SPEC에서 API 계약 추출 .claude/vibe/contracts/<feature>.md
16
- /vibe.contract check <feature> # 계약 vs 구현 비교, 드리프트 리포트
17
- /vibe.contract diff <feature> # 마지막 check 이후 변경된 필드만 요약
15
+ /vibe.contract extract <feature> # SPEC contract record at .claude/vibe/contracts/<feature>.md
16
+ /vibe.contract check <feature> # contract vs implementation, drift report
17
+ /vibe.contract diff <feature> # changed fields since last check
18
18
  ```
19
19
 
20
20
  ## What counts as an "API contract"
21
21
 
22
- 계약 = 외부(클라이언트, 다른 서비스) 의존하는 **모든 인터페이스 형태**:
22
+ A contract = any **interface shape** that external consumers (clients, other services) depend on:
23
23
 
24
24
  - HTTP endpoint: method + path + request schema + response schema + status codes
25
25
  - GraphQL: query/mutation name + args + return shape
26
- - 이벤트/메시지: topic + payload schema
27
- - 내보내진 TypeScript 함수 시그니처 (public API로 명시된 경우)
26
+ - Event/message: topic + payload schema
27
+ - Exported TypeScript function signature (when explicitly marked as public API)
28
28
 
29
29
  ## Process
30
30
 
31
31
  Load skill `vibe-contract` with subcommand: `$ARGUMENTS`
32
32
 
33
- **핵심 단계**:
33
+ **Core steps**:
34
34
 
35
- 1. **extract**: SPEC(.claude/vibe/specs/\<feature\>.md)의 "API" / "Endpoints" / "Interface" 섹션을 파싱, 구조화된 계약 레코드로 저장
36
- 2. **check**: 실제 구현(코드)에서 동일 엔드포인트를 찾아 시그니처/스키마 비교 드리프트 발견 P1 리포트
37
- 3. **diff**: 이전 check 스냅샷과 비교, **변경된 필드만** 노출 (노이즈 최소화)
35
+ 1. **extract**: parse SPEC sections like `## API` / `## Endpoints` / `## Interface` and persist as a structured contract record
36
+ 2. **check**: locate matching endpoints in the implementation, compare signature/schema, report drift as P1 findings
37
+ 3. **diff**: compare against the previous snapshot, surface only **changed fields** (noise minimized)
38
38
 
39
39
  ## Drift severity
40
40
 
41
- | Drift type | Severity | 예시 |
41
+ | Drift type | Severity | Example |
42
42
  |---|---|---|
43
- | Missing endpoint | P1 | SPEC에는 `GET /users/:id` 있는데 구현 없음 |
44
- | Missing required field in response | P1 | SPEC response `email` 있는데 구현에서 빠짐 |
43
+ | Missing endpoint | P1 | SPEC says `GET /users/:id`, implementation has none |
44
+ | Missing required field in response | P1 | SPEC response includes `email`, implementation drops it |
45
45
  | Type change (breaking) | P1 | `userId: number` → `userId: string` |
46
- | Added required request field | P1 | 기존 클라이언트 호환성 깨짐 |
47
- | Added optional field | P3 | 확장은 허용 |
48
- | Status code added | P2 | 클라이언트가 처리해야 케이스 |
49
- | Status code removed | P1 | 예상 응답이 사라짐 |
46
+ | Added required request field | P1 | breaks existing clients |
47
+ | Added optional field | P3 | extension is allowed |
48
+ | Status code added | P2 | client must handle a new case |
49
+ | Status code removed | P1 | expected response disappeared |
50
50
 
51
- **P1 드리프트 발견 시**: `/vibe.verify` 통과 여부와 무관하게 실패로 간주. 테스트는 통과해도 계약은 깨질 수 있기 때문.
51
+ **On any P1 drift**: treat as failure regardless of `/vibe.verify` outcome tests can pass while the contract breaks.
52
52
 
53
53
  ## Storage Format
54
54
 
55
55
  ```
56
56
  .claude/vibe/contracts/
57
- <feature>.md # 추출된 계약 (SSOT)
58
- <feature>.snapshot.md # 마지막 check 시점의 구현 스냅샷 (diff 비교용)
57
+ <feature>.md # extracted contract (SSOT)
58
+ <feature>.snapshot.md # implementation snapshot at last check (for diff)
59
59
  ```
60
60
 
61
61
  ### Contract schema (frontmatter)
@@ -81,24 +81,24 @@ endpoints:
81
81
 
82
82
  ## Integration with /vibe.verify
83
83
 
84
- `/vibe.verify <feature>` 흐름 끝에 자동 체인:
84
+ After `/vibe.verify <feature>` scenarios pass, auto-chain:
85
85
 
86
86
  ```
87
87
  scenarios pass → /vibe.contract check <feature>
88
88
  ├─ no drift → ✅ complete
89
- └─ drift found → ❌ report + auto-register to /vibe.regress (tag: integration)
89
+ └─ drift found → ❌ report + auto /vibe.regress register (tag: integration)
90
90
  ```
91
91
 
92
92
  ## Integration with /vibe.spec
93
93
 
94
- `/vibe.spec` 작성 완료 직후 자동으로 `/vibe.contract extract` 호출하여 계약을 미리 뽑아둠. 이후 `/vibe.run` 구현 시 이 계약이 참조점.
94
+ Right after `/vibe.spec` finishes writing the SPEC, auto-invoke `/vibe.contract extract`. The resulting contract becomes the reference for the subsequent `/vibe.run`.
95
95
 
96
96
  ## Done Criteria
97
97
 
98
- - [ ] `extract`는 SPEC에 API 섹션이 없으면 **에러 없이 스킵** (모든 feature API를 갖진 않음)
99
- - [ ] `check`는 드리프트 없으면 조용히 통과, 있으면 severity별 분류 출력
100
- - [ ] P1 드리프트는 반드시 `/vibe.regress register --from-contract` 자동 호출
101
- - [ ] `diff`는 이전 스냅샷이 없으면 " 실행"이라 안내
98
+ - [ ] `extract` exits cleanly when SPEC has no API section (not every feature has one)
99
+ - [ ] `check` is silent when no drift; otherwise prints findings grouped by severity
100
+ - [ ] Every P1 drift triggers `/vibe.regress register --from-contract`
101
+ - [ ] `diff` says "first run" when no prior snapshot exists
102
102
 
103
103
  ---
104
104
 
@@ -5,51 +5,51 @@ argument-hint: "register | generate | list | import | cluster [args]"
5
5
 
6
6
  # /vibe.regress
7
7
 
8
- **Regression Auto-Evolution** — 같은 버그를 잡지 않기 위한 도구.
8
+ **Regression Auto-Evolution** — never fix the same bug twice.
9
9
 
10
- > 버그는 기록되고, 예방 테스트는 자동 생성되고, 반복 패턴은 공통 테스트로 승격된다.
10
+ > Bugs are recorded, preventive tests are generated automatically, and recurring patterns get promoted into shared tests.
11
11
 
12
12
  ## Usage
13
13
 
14
14
  ```
15
- /vibe.regress register "<symptom>" # 수동 등록 (최소, 대부분 자동)
16
- /vibe.regress generate <slug> # bug → vitest 파일 생성
17
- /vibe.regress list # 미해결 목록
18
- /vibe.regress import # git log fix: 커밋 역추적
19
- /vibe.regress cluster # 3+ 유사 버그공통 테스트 제안
15
+ /vibe.regress register "<symptom>" # Manual register (rare most calls are automatic)
16
+ /vibe.regress generate <slug> # bug record → vitest file
17
+ /vibe.regress list # Open items
18
+ /vibe.regress import # Backfill from git log `fix:` commits
19
+ /vibe.regress cluster # 3+ similar bugspropose shared test
20
20
  ```
21
21
 
22
22
  ## Auto-integration
23
23
 
24
- - `/vibe.verify` 실패자동으로 `register` 호출 (수동 개입 없음)
25
- - `/vibe.run "<feature>"` 시작해당 feature의 미해결 회귀 항목 경고
24
+ - `/vibe.verify` failureauto-invokes `register` (no manual step)
25
+ - `/vibe.run "<feature>"` startwarns about open regressions for that feature
26
26
 
27
27
  ## Process
28
28
 
29
29
  Load skill `vibe-regress` with subcommand: `$ARGUMENTS`
30
30
 
31
- `vibe-regress` 스킬이 등록·생성·클러스터링을 수행.
31
+ The `vibe-regress` skill performs registration, generation, and clustering.
32
32
 
33
- **핵심 단계** (상세는 `skills/vibe-regress/SKILL.md` 참조):
33
+ **Core steps** (see `skills/vibe-regress/SKILL.md` for details):
34
34
 
35
- 1. 서브커맨드 파싱
36
- 2. `.claude/vibe/regressions/<slug>.md` 읽기/쓰기 (frontmatter 스키마 준수)
37
- 3. `generate` 프로젝트 테스트 스택 감지적합한 템플릿 선택 (vitest/jest)
38
- 4. `cluster` frontmatter의 `root-cause-tag`로 그룹핑3개 이상이면 공통 테스트 제안
39
- 5. `import` `git log --grep='^fix:'` 파싱 중복 스킵, 신규만 등록
35
+ 1. Parse subcommand
36
+ 2. Read/write `.claude/vibe/regressions/<slug>.md` (frontmatter schema enforced)
37
+ 3. On `generate`, detect the project's test stackchoose template (vitest / jest)
38
+ 4. On `cluster`, group by `root-cause-tag`; ≥3 entries propose a shared test
39
+ 5. On `import`, parse `git log --grep='^fix:'`; skip duplicates by commit hash
40
40
 
41
41
  ## Output
42
42
 
43
- - `.claude/vibe/regressions/<slug>.md` — 버그 레코드 (frontmatter + 증상/재현/근본원인)
44
- - 프로젝트 test dir — 생성된 vitest 파일 (`*.regression.test.ts` 네이밍)
45
- - `list` 서브커맨드는 터미널
43
+ - `.claude/vibe/regressions/<slug>.md` — bug record (frontmatter + reproduction / root cause)
44
+ - Project test dir — generated vitest file (`*.regression.test.ts`)
45
+ - `list` prints a terminal table
46
46
 
47
47
  ## Storage Format
48
48
 
49
49
  ```markdown
50
50
  ---
51
51
  slug: login-jwt-expiry-off-by-one
52
- symptom: "JWT 만료 시간이 1초 일찍 끊김"
52
+ symptom: "JWT expiry cuts off one second early"
53
53
  root-cause-tag: timezone
54
54
  fix-commit: abc1234
55
55
  test-path: src/auth/__tests__/login.regression.test.ts
@@ -48,18 +48,18 @@ Execute **Scenario-Driven Implementation** with automatic quality verification.
48
48
 
49
49
  ### Pre-Run Regression Check (MANDATORY, before implementation starts)
50
50
 
51
- 시작 직후 필수 실행:
51
+ Run immediately after start:
52
52
 
53
53
  ```
54
54
  Load skill `vibe-regress` with: list --feature "{feature-name}"
55
55
  ```
56
56
 
57
- - 미해결 회귀 항목 있으면:
58
- - interactive 모드: 사용자에게 "먼저 회귀 테스트 생성 진행?" 묻기
59
- - ultrawork 모드: 자동 `/vibe.regress generate <slug>`로 예방 테스트 생성 후 진행
60
- - 미해결 없으면 조용히 통과
57
+ - If any open regressions exist:
58
+ - interactive mode: ask the user "generate preventive tests first, then proceed?"
59
+ - ultrawork mode: auto-invoke `/vibe.regress generate <slug>` for each, then proceed
60
+ - No open regressions → silently continue
61
61
 
62
- 또한 `.claude/vibe/contracts/{feature-name}.md`이 있으면 로드구현 계약 준수 기준으로 사용.
62
+ Also load `.claude/vibe/contracts/{feature-name}.md` if presentuse it as the contract reference during implementation.
63
63
 
64
64
  ### Core Flow
65
65
 
@@ -372,15 +372,15 @@ Load skill `vibe-spec-review` with feature: {feature-name}
372
372
  5. Review Debate Team (2+ P1/P2 이슈 시)
373
373
  6. 사용자 최종 체크포인트
374
374
 
375
- ### Phase 4.5: Contract Extract (자동, API 있는 feature만)
375
+ ### Phase 4.5: Contract Extract (auto, only for features with an API)
376
376
 
377
377
  ```
378
378
  Load skill `vibe-contract` with: extract "{feature-name}"
379
379
  ```
380
380
 
381
- SPEC `## API` / `## Endpoints` / `## Interface` 섹션이 있으면 계약을 `.claude/vibe/contracts/{feature-name}.md`로 추출. 섹션이 없으면 에러 없이 스킵 (모든 feature API를 갖지 않음).
381
+ If the SPEC has a `## API` / `## Endpoints` / `## Interface` section, extract the contract to `.claude/vibe/contracts/{feature-name}.md`. If the section is absent, exit cleanly (not every feature has an API).
382
382
 
383
- 계약은 Phase 5a 구현 참조되고, `/vibe.verify` 종료 drift 검사에 사용됨.
383
+ The contract is referenced during Phase 5a implementation, and used by `/vibe.verify` for drift detection.
384
384
 
385
385
  ### Phase 5a: Logic Track
386
386
 
@@ -388,9 +388,9 @@ SPEC에 `## API` / `## Endpoints` / `## Interface` 섹션이 있으면 계약을
388
388
  /vibe.run "{feature-name}"
389
389
  ```
390
390
 
391
- SPEC → 코드 구현. 시작 시 자동 체크:
392
- - `/vibe.regress list --feature {feature-name}` — 미해결 회귀 항목 있으면 경고
393
- - `.claude/vibe/contracts/{feature-name}.md` — 있으면 로드하여 구현 가이드
391
+ SPEC → code. Auto-checks at start:
392
+ - `/vibe.regress list --feature {feature-name}` — warn if any open regressions exist
393
+ - `.claude/vibe/contracts/{feature-name}.md` — load if present, use as implementation guide
394
394
 
395
395
  ### Phase 5b: UI Track (type ∈ {website, webapp, mobile}일 때만)
396
396
 
@@ -0,0 +1,96 @@
1
+ ---
2
+ description: Self-test vibe across CC and coco — verify every command/skill/hook/agent/tool is callable and behaves identically
3
+ argument-hint: "parity | report | compare [args]"
4
+ ---
5
+
6
+ # /vibe.test
7
+
8
+ **Vibe Self-Test** — verify vibe works identically in both Claude Code and coco.
9
+
10
+ > Catch features broken on one harness before users do.
11
+
12
+ ## Usage
13
+
14
+ ```
15
+ /vibe.test parity # Static parity (file set + content sync) — local, fast
16
+ /vibe.test report # Invoke every feature in current harness, write JSON+MD report
17
+ /vibe.test compare <cc-report> <coco-report> # Diff two reports, classify P1/P2/P3
18
+ ```
19
+
20
+ ## Key Constraint
21
+
22
+ `/vibe.test report` only tests the **harness it runs in**. Run from CC for CC results, run from coco for coco results. Then `compare` merges them.
23
+
24
+ ```
25
+ [CC] /vibe.test report → .claude/vibe/test-reports/<ts>-cc.{json,md}
26
+ [coco] /vibe.test report → .coco/vibe/test-reports/<ts>-coco.{json,md}
27
+ [any] /vibe.test compare → diff with parity findings
28
+ ```
29
+
30
+ ## Subcommand: parity (static check, stage 1)
31
+
32
+ No harness execution — file system comparison only:
33
+
34
+ | Check | Compared |
35
+ |---|---|
36
+ | **install set** | `~/.claude/{commands,skills,agents}/` vs `~/.coco/{commands,skills,agents}/` file set |
37
+ | **content sync** | `CLAUDE.md` ↔ `AGENTS.md` body (excluding header/meta blocks) |
38
+ | **path config** | `.claude/vibe/` vs `.coco/vibe/` directory layout |
39
+ | **doc references** | Paths cited in CLAUDE.md/AGENTS.md actually resolve in install dir |
40
+
41
+ **Output**: console table + `.claude/vibe/test-reports/<ts>-parity.json`
42
+
43
+ This stage alone catches:
44
+ - New commands missing on one harness (e.g. if `/vibe.regress` had been added only to CC)
45
+ - AGENTS.md holding stale paths (e.g. `.codex/` references after a coco rename)
46
+ - CLAUDE.md ↔ AGENTS.md body drift
47
+
48
+ ## Subcommand: report (runtime invocation)
49
+
50
+ Probes every shipped feature in the current harness and writes a JSON+MD report.
51
+
52
+ | Category | Probe |
53
+ |---|---|
54
+ | commands | frontmatter validity, body delegates to a skill |
55
+ | skills | frontmatter validity, triggers non-empty |
56
+ | hooks | run matching vitest suite |
57
+ | agents | frontmatter validity, declared tools exist in harness |
58
+ | tools | run matching vitest suite or smoke-call with minimal input |
59
+
60
+ No external LLM calls. Interactive commands are not actually invoked — structural validation only. See `skills/vibe-test/SKILL.md` for full probe spec and failure-handling rules.
61
+
62
+ ## Subcommand: compare (diff two reports)
63
+
64
+ Compare two JSON reports and classify findings:
65
+ - **P1**: feature exists on only one side → missing
66
+ - **P2**: both sides have it but response shape differs → behavioral drift
67
+ - **P3**: only message wording differs, semantics identical → informational
68
+
69
+ P1 findings auto-invoke `/vibe.regress register --from-test`.
70
+
71
+ ## Process
72
+
73
+ Load skill `vibe-test` with subcommand: `$ARGUMENTS`
74
+
75
+ See `skills/vibe-test/SKILL.md` for detailed logic.
76
+
77
+ ## Storage
78
+
79
+ ```
80
+ .claude/vibe/test-reports/ (CC side)
81
+ .coco/vibe/test-reports/ (coco side)
82
+ <YYYYMMDD-HHmm>-<harness>.json
83
+ <YYYYMMDD-HHmm>-<harness>.md
84
+ <YYYYMMDD-HHmm>-compare.md (compare output)
85
+ ```
86
+
87
+ ## Done Criteria
88
+
89
+ - [ ] `parity` runs without external calls — local file inspection only (fast, deterministic)
90
+ - [ ] If only one install dir exists, exit cleanly with guidance (not an error)
91
+ - [ ] `compare` warns when reports are not within ±1 minute of each other (timing drift = false positives)
92
+ - [ ] P1 drift auto-registers via `/vibe.regress`
93
+
94
+ ---
95
+
96
+ ARGUMENTS: $ARGUMENTS
@@ -235,9 +235,9 @@ For each failed scenario:
235
235
  location: {file:line}
236
236
  ```
237
237
 
238
- - `--from-verify` 모드는 사용자 확인 없이 등록 (verify 실패 컨텍스트에서 마찰 최소화)
239
- - 등록된 bug slug Failure Report "Fix" 섹션에 링크로 노출
240
- - 이후 `/vibe.regress generate <slug>`로 예방 테스트 생성 가능
238
+ - `--from-verify` mode skips user confirmation (the user is already attentive in a verify-failure context; minimize friction)
239
+ - The registered bug's slug appears as a link in the Failure Report's "Fix" section
240
+ - Follow up with `/vibe.regress generate <slug>` to produce a preventive test
241
241
 
242
242
  ### Failure Report
243
243
 
@@ -402,18 +402,18 @@ node -e "import('{{VIBE_PATH_URL}}/node_modules/@su-record/vibe/dist/tools/index
402
402
  **Codex P2 발견 시:**
403
403
  - TODO 파일에 기록 후 완료 처리
404
404
 
405
- ## Post-Verify Contract Check (자동, contract 파일 있을 때만)
405
+ ## Post-Verify Contract Check (auto, only when a contract file exists)
406
406
 
407
- 모든 시나리오 통과 자동 호출:
407
+ After all scenarios pass, auto-invoke:
408
408
 
409
409
  ```
410
410
  Load skill `vibe-contract` with: check "{feature-name}"
411
411
  ```
412
412
 
413
- - `.claude/vibe/contracts/{feature-name}.md`이 없으면 스킵
414
- - drift 없음 → verify 통과 유지
415
- - **P1 drift** → verify 실패로 강등 + `/vibe.regress register --from-contract` 자동 호출
416
- - P2/P3 drift → 경고만, 통과 유지
413
+ - Skip if `.claude/vibe/contracts/{feature-name}.md` does not exist
414
+ - No drift → verify still passes
415
+ - **P1 drift** → demote verify to fail; auto-call `/vibe.regress register --from-contract`
416
+ - P2 / P3 drift → warning only; verify still passes
417
417
 
418
418
  ## Next Step
419
419
 
@@ -280,8 +280,89 @@ describe('pre-tool-guard', () => {
280
280
  tool_name: 'Bash',
281
281
  tool_input: { command: 'DROP TABLE users' },
282
282
  });
283
- // tool_input is stringified — pattern matches against the JSON string
284
283
  expect(result.exitCode).toBe(2);
284
+ expect(result.stdout).toContain('Database drop detected');
285
+ });
286
+ });
287
+
288
+ // ══════════════════════════════════════════════════
289
+ // Regression: file content false positives (issue: machine-key.ts blocked)
290
+ // .claude/vibe/regressions/pre-tool-guard-content-false-positive.md
291
+ //
292
+ // 이전 구현은 tool_input 전체를 JSON.stringify해서 패턴 매칭했기 때문에
293
+ // 파일 내용에 '/etc/', '.env', 'secret' 같은 리터럴이 있으면 차단됐음.
294
+ // write/edit 패턴은 file_path만 봐야 한다.
295
+ // ══════════════════════════════════════════════════
296
+ describe('regression: write/edit content must not trigger path patterns', () => {
297
+ it('should ALLOW writing safe path even when content contains "/etc/" literal', () => {
298
+ const result = runGuardWithStdin({
299
+ tool_name: 'Write',
300
+ tool_input: {
301
+ file_path: 'src/machine-key.ts',
302
+ content: "for (const path of ['/etc/machine-id', '/var/lib/dbus/machine-id']) {}",
303
+ },
304
+ });
305
+ expect(result.exitCode).toBe(0);
306
+ expect(result.stdout).not.toContain('Writing to system directory');
307
+ });
308
+
309
+ it('should ALLOW writing safe path even when content contains "/usr/" literal', () => {
310
+ const result = runGuardWithStdin({
311
+ tool_name: 'Write',
312
+ tool_input: {
313
+ file_path: 'src/cli-detect.ts',
314
+ content: "const IOREG = '/usr/sbin/ioreg';",
315
+ },
316
+ });
317
+ expect(result.exitCode).toBe(0);
318
+ expect(result.stdout).not.toContain('Writing to system directory');
319
+ });
320
+
321
+ it('should ALLOW writing safe path even when content mentions ".env" / "secret"', () => {
322
+ const result = runGuardWithStdin({
323
+ tool_name: 'Write',
324
+ tool_input: {
325
+ file_path: 'src/config.ts',
326
+ content: "// loads from .env, never log secret values",
327
+ },
328
+ });
329
+ expect(result.exitCode).toBe(0);
330
+ expect(result.stdout).not.toContain('Writing to sensitive file');
331
+ });
332
+
333
+ it('should ALLOW editing safe path even when new_string contains ".env" literal', () => {
334
+ const result = runGuardWithStdin({
335
+ tool_name: 'Edit',
336
+ tool_input: {
337
+ file_path: 'src/index.ts',
338
+ old_string: 'const x = 1',
339
+ new_string: '// reads .env at startup',
340
+ },
341
+ });
342
+ expect(result.exitCode).toBe(0);
343
+ expect(result.stdout).not.toContain('Editing sensitive file');
344
+ });
345
+
346
+ it('should still BLOCK Write when file_path itself targets /etc/', () => {
347
+ const result = runGuardWithStdin({
348
+ tool_name: 'Write',
349
+ tool_input: { file_path: '/etc/passwd', content: 'root:x:0:0' },
350
+ });
351
+ expect(result.exitCode).toBe(2);
352
+ expect(result.stdout).toContain('Writing to system directory');
353
+ });
354
+
355
+ it('should still WARN Edit when file_path itself is a credentials file', () => {
356
+ const result = runGuardWithStdin({
357
+ tool_name: 'Edit',
358
+ tool_input: {
359
+ file_path: 'config/credentials.json',
360
+ old_string: 'a',
361
+ new_string: 'b',
362
+ },
363
+ });
364
+ expect(result.exitCode).toBe(0);
365
+ expect(result.stdout).toContain('Editing sensitive file');
285
366
  });
286
367
  });
287
368
  });
@@ -7,26 +7,35 @@
7
7
  import { VIBE_PATH, PROJECT_DIR } from './utils.js';
8
8
 
9
9
  // 위험한 명령어 패턴
10
+ //
11
+ // 각 엔트리의 `target`은 매칭 대상 필드:
12
+ // - 'command' → tool_input.command (Bash)
13
+ // - 'file_path' → tool_input.file_path (Write, Edit)
14
+ // - 'raw' → 전체 문자열 (스키마 모를 때 폴백)
15
+ //
16
+ // 예전 버전은 항상 stringify된 tool_input 전체에 매칭하여 file content가
17
+ // 패턴(예: /etc/, .env)과 일치하면 false positive로 차단되는 버그가 있었음.
18
+ // 회귀: .claude/vibe/regressions/pre-tool-guard-content-false-positive.md
10
19
  const DANGEROUS_PATTERNS = {
11
20
  bash: [
12
- { pattern: /rm\s+-rf?\s+[\/~]/, severity: 'critical', message: 'Deleting root or home directory' },
13
- { pattern: /rm\s+-rf?\s+\*/, severity: 'high', message: 'Wildcard deletion detected' },
14
- { pattern: /git\s+push\s+.*--force/, severity: 'high', message: 'Force push detected' },
15
- { pattern: /git\s+reset\s+--hard/, severity: 'medium', message: 'Hard reset will discard changes' },
16
- { pattern: /drop\s+(table|database)/i, severity: 'critical', message: 'Database drop detected' },
17
- { pattern: /truncate\s+table/i, severity: 'high', message: 'Table truncate detected' },
18
- { pattern: /:(){ :|:& };:/, severity: 'critical', message: 'Fork bomb detected' },
19
- { pattern: /mkfs|fdisk|dd\s+if=/, severity: 'critical', message: 'Disk operation detected' },
20
- { pattern: /chmod\s+-R\s+777/, severity: 'medium', message: 'Insecure permission change' },
21
- { pattern: /curl.*\|\s*(ba)?sh/, severity: 'high', message: 'Piping curl to shell' },
21
+ { pattern: /rm\s+-rf?\s+[\/~]/, target: 'command', severity: 'critical', message: 'Deleting root or home directory' },
22
+ { pattern: /rm\s+-rf?\s+\*/, target: 'command', severity: 'high', message: 'Wildcard deletion detected' },
23
+ { pattern: /git\s+push\s+.*--force/, target: 'command', severity: 'high', message: 'Force push detected' },
24
+ { pattern: /git\s+reset\s+--hard/, target: 'command', severity: 'medium', message: 'Hard reset will discard changes' },
25
+ { pattern: /drop\s+(table|database)/i, target: 'command', severity: 'critical', message: 'Database drop detected' },
26
+ { pattern: /truncate\s+table/i, target: 'command', severity: 'high', message: 'Table truncate detected' },
27
+ { pattern: /:(){ :|:& };:/, target: 'command', severity: 'critical', message: 'Fork bomb detected' },
28
+ { pattern: /mkfs|fdisk|dd\s+if=/, target: 'command', severity: 'critical', message: 'Disk operation detected' },
29
+ { pattern: /chmod\s+-R\s+777/, target: 'command', severity: 'medium', message: 'Insecure permission change' },
30
+ { pattern: /curl.*\|\s*(ba)?sh/, target: 'command', severity: 'high', message: 'Piping curl to shell' },
22
31
  ],
23
32
  edit: [
24
- { pattern: /\.env|credentials|secret|password|api[_-]?key/i, severity: 'medium', message: 'Editing sensitive file' },
25
- { pattern: /package-lock\.json|yarn\.lock|pnpm-lock/, severity: 'low', message: 'Editing lock file directly' },
33
+ { pattern: /\.env|credentials|secret|password|api[_-]?key/i, target: 'file_path', severity: 'medium', message: 'Editing sensitive file' },
34
+ { pattern: /package-lock\.json|yarn\.lock|pnpm-lock/, target: 'file_path', severity: 'low', message: 'Editing lock file directly' },
26
35
  ],
27
36
  write: [
28
- { pattern: /\.env|credentials|secret/i, severity: 'medium', message: 'Writing to sensitive file' },
29
- { pattern: /\/etc\/|\/usr\/|C:\\Windows/i, severity: 'critical', message: 'Writing to system directory' },
37
+ { pattern: /\.env|credentials|secret/i, target: 'file_path', severity: 'medium', message: 'Writing to sensitive file' },
38
+ { pattern: /\/etc\/|\/usr\/|C:\\Windows/i, target: 'file_path', severity: 'critical', message: 'Writing to system directory' },
30
39
  ],
31
40
  };
32
41
 
@@ -82,10 +91,39 @@ const SAFE_ALTERNATIVES = {
82
91
  'chmod 777': 'Use specific permissions (e.g., chmod 755 for directories)',
83
92
  };
84
93
 
94
+ /**
95
+ * 패턴 매칭 대상 추출
96
+ *
97
+ * tool_input이 객체이면 target 필드만 뽑아 반환. 객체가 아니거나(레거시 argv 모드)
98
+ * target='raw'이면 입력 전체를 그대로 반환.
99
+ *
100
+ * 핵심: write/edit 패턴은 file_path만 봐야 한다. content에 우연히 들어간
101
+ * 리터럴(예: 코드 안의 '/etc/machine-id' 문자열)이 차단을 일으키지 않도록.
102
+ */
103
+ function extractTarget(rawInput, target) {
104
+ if (target === 'raw') return typeof rawInput === 'string' ? rawInput : JSON.stringify(rawInput);
105
+
106
+ // 객체로 파싱 시도
107
+ let obj = rawInput;
108
+ if (typeof rawInput === 'string') {
109
+ try {
110
+ obj = JSON.parse(rawInput);
111
+ } catch {
112
+ // JSON 아님 → 레거시 argv 모드. argv[3]은 보통 file_path 또는 command 단일 문자열.
113
+ // 안전하게 그 문자열을 target 값으로 간주.
114
+ return rawInput;
115
+ }
116
+ }
117
+
118
+ if (typeof obj !== 'object' || obj === null) return '';
119
+ const value = obj[target];
120
+ return typeof value === 'string' ? value : '';
121
+ }
122
+
85
123
  /**
86
124
  * 명령어 검증
87
125
  */
88
- function validateCommand(toolName, input) {
126
+ function validateCommand(toolName, rawInput) {
89
127
  const results = {
90
128
  allowed: true,
91
129
  severity: 'none',
@@ -95,8 +133,11 @@ function validateCommand(toolName, input) {
95
133
 
96
134
  const patterns = DANGEROUS_PATTERNS[toolName.toLowerCase()] || [];
97
135
 
98
- for (const { pattern, severity, message } of patterns) {
99
- if (pattern.test(input)) {
136
+ for (const { pattern, target, severity, message } of patterns) {
137
+ const haystack = extractTarget(rawInput, target || 'raw');
138
+ if (!haystack) continue;
139
+
140
+ if (pattern.test(haystack)) {
100
141
  results.warnings.push(`[${severity.toUpperCase()}] ${message}`);
101
142
 
102
143
  // 심각도에 따른 처리
@@ -109,9 +150,9 @@ function validateCommand(toolName, input) {
109
150
  results.severity = severity;
110
151
  }
111
152
 
112
- // 대안 제안
153
+ // 대안 제안 — 매칭된 target 필드에서만 검색
113
154
  for (const [dangerous, safe] of Object.entries(SAFE_ALTERNATIVES)) {
114
- if (input.includes(dangerous)) {
155
+ if (haystack.includes(dangerous)) {
115
156
  results.suggestions.push(safe);
116
157
  }
117
158
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@su-record/vibe",
3
- "version": "2.9.20",
3
+ "version": "2.9.22",
4
4
  "description": "AI Coding Framework for Claude Code — 56 agents, 45 skills, multi-LLM orchestration",
5
5
  "type": "module",
6
6
  "main": "dist/cli/index.js",