Skip to content

Commit 05ad59a

Browse files
authored
release: v0.4.7 — policy conditions, UX overhaul, dashboard, security hardening (#90)
* chore: ignore .home/ (Codex sandbox artifact) * fix(dashboard): conn-dot states, empty states, mobile overflow, tab fade, UX polish - Wire conn-dot CSS classes (ok/err/wait) in setConnected() and SSE error handler - Empty states: icons + descriptive subtitles for pending, history, denials - Mobile: flex-wrap at ≤540px for pend-item, policy-test-row; bulk-bar fix at ≤420px - Tab fade-in animation (tabFadeIn, 0.15s) - Token input: show/hide toggle button (👁) - Recent Denials: 'View all →' link switches to History tab - Pending buttons: 'Always' → 'Always Allow' - Dangerous items: 3px border + background tint - Denial rows: title attribute for full reason on hover - History: loading indicator (hist-loading div, shown/hidden around fetch) * feat(policy): agent_depth and tool_param_matches conditions - engine/decision.go: add AgentDepth int and Input map[string]any to ToolCall - engine/policy.go: add AgentDepth *IntRangeCondition and ToolParamMatches map[string]string to Condition; new IntRangeCondition type (gte/lte/eq); IsEmpty() updated - engine/matcher.go: wire both conditions in ExplainCondition and matchCondition; agent_depth range/eq checks; tool_param_matches case-insensitive glob against call.Input - engine/lint.go: register agent_depth and tool_param_matches as valid condition keys; fix gofmt whitespace - engine/matcher_test.go: 9 test cases for TestMatchCondition_AgentDepth and TestMatchCondition_ToolParamMatches (all passing) - cmd/rampart/cli/hook.go: read RAMPART_AGENT_DEPTH env; increment depth for tool==agent; set call.AgentDepth and call.Input - internal/proxy/server.go: add Input field to ToolExecRequest; populate via extractToolInput() (MCP arguments/tool_input/params fallback) - internal/mcp/proxy.go: set ToolCall.Input = params.Arguments on tools/call - docs-site/reference/policy-schema.md: document agent_depth and tool_param_matches with examples * fix(polish): token show command, doctor path hints, default_action:allow lint warning * fix(polish): token show command, doctor path hints, default_action:allow lint warning - token.go: new 'rampart token' / 'rampart token show' command; reads ~/.rampart/token via readPersistedToken() - token_test.go: TestTokenShow_PrintsPersistedToken covers both 'token' and 'token show' variants - doctor.go: hook failure messages now include the actual file/directory path that was checked, plus 'Run: rampart setup <agent>' hint for Claude Code and Cline - doctor_test.go: TestDoctorHooks_PathHints asserts path appears in failure messages - lint.go: LintWarning when default_action is 'allow' — advises using deny + explicit allow rules - lint_test.go: TestLint_DefaultActionAllowWarning asserts warning is emitted * chore: remove Codex sandbox cache artifacts, ignore .home/ * fix(ux): hook fail-closed warning, policy explain auto-discover, watch token/URL auto, --version flag, status fixes * fix(ux): hook fail-closed warning, policy explain auto-discover, watch token/URL auto, --version flag, status unknown fix, upgrade restart reminder - hook_approval.go: always print WARNING when serve is unreachable; route all stderr through injected errWriter - hook.go: pass cmd.ErrOrStderr() to approval client - policy.go: resolveExplainPolicyPath() — auto-discovers ~/.rampart/policies/standard.yaml, then cwd rampart.yaml, then helpful error - watch.go: resolveWatchServeConfig() — auto-discovers URL (defaults localhost:9090) and token (~/.rampart/token) - root.go: --version persistent flag, delegates to shared writeVersion() - version.go: extract writeVersion(io.Writer) helper - status.go: suppress '(unknown)' parenthetical when cmd or policy is empty/unknown - upgrade.go: print restart reminder after successful upgrade - Tests: 12 new test cases covering all changes (cli_test, policy_test, watch_test, status_test, upgrade_test, hook_approval_test) * fix(ux): resolveExplainPolicyPath respects programmatically-set configPath When opts.configPath is set directly (not via cobra --config flag), cobra's Changed() returns false and auto-discovery would override the intended path. Now: if configPath is non-empty, non-default, and the file exists, use it — handles both the cobra-flag path and programmatic/test usage correctly. Fixes TestPolicyExplainDeny CI failure on fix/v047-ux. * fix(reliability): audit permissions check, reload error logging, PostToolUseFailure audit, dead code cleanup - doctor.go: scan ~/.rampart/audit/ for world-readable files (mode & 0o004); warn in summary - rules_handlers.go: log engine.Reload() errors at Error level (was silently discarded in 2 places) - server.go: same Reload() fix in policy hot-reload handler - hook.go: write audit event before PostToolUseFailure early return - watch.go: remove dead approvalLines variable and assignments - mcp/proxy.go: close childIn on all exit paths in child error handler * feat: prompt injection detection + actionable denial hints policies/standard.yaml: new watch-prompt-injection policy - Monitors fetch/web_search/read/exec/mcp tool responses for injection patterns - Covers: instruction override, role hijack, model-specific tokens (<|im_start|>system, [SYSTEM], ###INSTRUCTIONS###), exfiltration directives - action: watch (not deny) — logs for review without blocking legitimate content - 14 regex patterns, all case-insensitive via (?i), tested against 15 cases cmd/rampart/cli/hook.go: enrich PostToolUseFailure feedback - Includes tool name in suggested explain command - Adds: 'run rampart policy explain <tool>', 'rampart watch', policy path hint, and link to rampart.sh/docs/exceptions - Agent can now surface concrete next steps to the user instead of hitting a dead end * fix: code review fixes + agent-install (PR #90 follow-up) (#91) * fix: code review fixes + agent-install support (PR #90 follow-up) Fixes from code review of PR #90: B1 (BLOCKER): tool_param_matches uses MatchGlob instead of filepath.Match - filepath.Match treats * as non-separator, so '**/.env*' silently never matched multi-directory paths like '/home/user/.envrc' - Both ExplainCondition and matchCondition now use MatchGlob - Add two tests with ** patterns that would have caught this W1: Clamp RAMPART_AGENT_DEPTH to >= 0 - strconv.Atoi parses negative strings fine; a manipulated env var could set depth=-999 to bypass agent_depth: {gte: 1} deny rules - One-line clamp after parse W2: PostToolUseFailure audit event action 'allow' -> 'feedback' - Logging a blocked tool call as action:'allow' was semantically wrong, skewed allow/deny stats in the dashboard, and confused audit readers - New action:'feedback' clearly identifies these as guidance-injection events W3: resolveExplainPolicyPath errors on non-default missing path - Previously silently fell through to auto-discovery when a programmatic configPath pointed at a file that didn't exist - Now returns an explicit error, consistent with --config flag behavior W4: Reload() failure returns HTTP 500 instead of silently 200 - Both the delete and write paths now return InternalServerError if engine.Reload() fails post-change, so callers know enforcement may not reflect the written state feat: OpenClaw auto-detection and agent-install support - detectEnv() now detects OpenClaw via OPENCLAW_SERVICE_MARKER env var (set by the OpenClaw gateway when it spawns an agent process); more reliable than PATH lookup which was too broad - quickstart --yes / -y flag: non-interactive mode for AI agents, CI, and scripted setups; also auto-enables --patch-tools for OpenClaw - Add docs/guides/agent-install.md: step-by-step guide written for an AI agent to read and execute when told 'install Rampart and protect me' - Add 'Security Guides' nav section in mkdocs.yml - Update integrations/openclaw.md with quickstart --yes callout - Add TestDetectEnv_OpenClaw; fix existing detectEnv tests to unset OPENCLAW_SERVICE_MARKER so they pass on machines running OpenClaw * fix(docs): agent-install accuracy fixes - rampart test does not exist; replace with rampart doctor - rampart status expected output was wrong; show real format (Protected/Mode/Today, not 'rampart serve running port') - --yes flag description now accurately says what it does: enables --patch-tools for OpenClaw, safe no-op for others - Step numbering fixed (removed step 4 gap after deleting rampart test) - Reference table: remove rampart test row, fix status description * docs: SEO + agent-friendly docs rewrite (#92) * docs: SEO + agent-friendly docs rewrite - Add description: frontmatter to all 20+ docs-site pages (these become HTML meta description tags and Google search snippets) - New guides/securing-claude-code.md: standalone SEO guide targeting 'claude code security', 'how to keep claude code safe', etc. - New guides/prompt-injection.md: covers watch-prompt-injection policy, detection patterns, and how to escalate to deny - Homepage FAQ: literal search-query questions as h2s with Rampart answers - README: opens with 'security layer for AI coding agents' framing - Integrations claude-code.md: 'Why You Need This' section with concrete attack scenarios (rm -rf, curl|bash, ssh key exfil, prompt injection) and 'What Gets Blocked by Default' table - mkdocs.yml: Security Guides nav section with new guides * fix(docs): review feedback — FAQ, escalate YAML, description length - FAQ: replace 'Does Rampart work with OpenClaw?' with the more broadly useful 'Does Rampart send my commands to any external server?' (biggest adoption blocker for security-conscious users; answer: no, everything is local) - FAQ securing-claude-code.md Q4: clarify hook unreachable behavior — Rampart prints WARNING to stderr, falls back to hookAsk (native Claude Code permission prompt), not silently fail-open - Fix escalate-to-deny YAML in prompt-injection.md: was wrong schema (action/tool at top level, wrong field name 'response_patterns'); now uses correct nested rules: / match: / response_matches: structure - Trim claude-code.md description: 201 → 158 chars (Google truncates at 160) * docs: replace Mermaid diagrams and architecture.png with D2 (theme 200) (#93) - Drop all 4 Mermaid diagrams (policy-engine, integrations, mcp-proxy, semantic-verification) in favour of D2 with theme 200 + ELK layout - Replace static architecture.png (invisible to LLMs) with inline D2 diagram showing agents → interception → policy engine → outcomes - Remove emojis from node labels; colour alone carries semantic meaning - Rewrite integrations decision tree: cleaner branch labels, distinct nodes for wrap vs preload, removes confusing 'Has $SHELL support?' fork - Add mkdocs-d2-plugin to mkdocs.yml (theme 200, elk layout, pad 40) - Update docs CI workflow: install D2 binary + mkdocs-d2-plugin * docs: README architecture diagram → D2-rendered SVG (#94) - docs/architecture.d2: canonical D2 source (theme 200, elk layout) - docs/architecture.svg: pre-rendered SVG, embedded in README as <img> - Replace 'How it works' Mermaid block with SVG embed — looks significantly better on GitHub, also agent/LLM readable (SVG is text) - Strip emojis from approval flow Mermaid node labels - Add .github/workflows/render-diagrams.yml: auto re-renders architecture.svg and commits when docs/architecture.d2 changes * fix: sweep warnings — .env.* policy coverage + agent-install note cleanup (#96) W1: collapse OpenClaw restart admonition into prose in agent-install.md (same content, less visual weight) W2: expand .env credential coverage to include .env.* variants (.env.local, .env.production, .env.staging etc. were not blocked) - block-credential-access (read): add **/.env.* pattern - block-credential-commands (exec): add cat **/.env.* - block-sensitive-writes (write/edit): add **/.env.* - Update docs tables in claude-code.md and securing-claude-code.md
1 parent b6ce72b commit 05ad59a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+2214
-196
lines changed

.github/workflows/docs.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,17 @@ jobs:
2222
with:
2323
python-version: '3.12'
2424

25-
- run: pip install mkdocs-material mkdocs-minify-plugin
25+
- run: pip install mkdocs-material mkdocs-minify-plugin mkdocs-d2-plugin
26+
27+
- name: Install D2
28+
run: curl -fsSL https://d2lang.com/install.sh | sh -s --
29+
env:
30+
SHELL: /bin/bash
2631

2732
- name: Build docs
2833
run: mkdocs build
34+
env:
35+
PATH: /home/runner/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
2936

3037
# Deploy to peg/rampart-docs (the repo that serves docs.rampart.sh via GitHub Pages).
3138
# Requires a PAT with contents:write on peg/rampart-docs stored as DOCS_DEPLOY_TOKEN.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Render Diagrams
2+
3+
on:
4+
push:
5+
branches: [main, staging]
6+
paths:
7+
- 'docs/architecture.d2'
8+
9+
permissions:
10+
contents: write
11+
12+
jobs:
13+
render:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
17+
18+
- name: Install D2
19+
run: curl -fsSL https://d2lang.com/install.sh | sh -s --
20+
env:
21+
SHELL: /bin/bash
22+
23+
- name: Render architecture.svg
24+
run: |
25+
export PATH="$HOME/.local/bin:$PATH"
26+
d2 --theme 200 --layout elk --pad 40 \
27+
docs/architecture.d2 \
28+
docs/architecture.svg
29+
30+
- name: Commit updated SVG
31+
run: |
32+
git config user.name "github-actions[bot]"
33+
git config user.email "github-actions[bot]@users.noreply.github.com"
34+
git add docs/architecture.svg
35+
git diff --staged --quiet || git commit -m "chore: re-render architecture.svg from docs/architecture.d2"
36+
git push

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ coverage.out
2525
.cache/
2626
site/
2727
reddit-post.md
28+
.home/

README.md

Lines changed: 6 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# 🛡️ Rampart
44

5-
**See everything your AI agent does. Block the dangerous stuff.**
5+
**The security layer for AI coding agents.**
66

77
[![Go](https://img.shields.io/badge/Go-1.24+-00ADD8?style=flat&logo=go)](https://go.dev)
88
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
@@ -14,15 +14,11 @@
1414

1515
---
1616

17-
Rampart is a runtime policy engine for AI coding agents. Every tool call — bash commands, file reads, file writes, HTTP fetches — is evaluated against your YAML policies before it executes. Dangerous calls are blocked in microseconds. Everything is written to a hash-chained audit trail. Ambiguous calls can be held for human approval.
18-
19-
It works in `--dangerously-skip-permissions` mode. [Full setup guide →](https://docs.rampart.sh/getting-started/quickstart/)
20-
21-
### Get started in one command
17+
Claude Code's `--dangerously-skip-permissions` mode — and similar autonomous modes in Cursor, Cline, and Codex — give agents unrestricted shell access. Rampart sits between the agent and your system: every command, file access, and network request is evaluated against your YAML policy before it executes. Dangerous commands are blocked in microseconds. Everything is logged.
2218

19+
One command to get protected:
2320
```bash
24-
# Install and set up in one command:
25-
curl -fsSL https://rampart.sh/install | bash
21+
rampart setup claude-code
2622
```
2723

2824
`rampart quickstart` auto-detects Claude Code, Cursor, or Windsurf, installs `rampart serve` as a boot service, configures hooks, and runs a health check. Done.
@@ -48,39 +44,7 @@ Once running, every command Claude executes goes through Rampart's policy engine
4844

4945
## How it works
5046

51-
```mermaid
52-
graph LR
53-
subgraph "AI Agents"
54-
CC[Claude Code]
55-
CL[Cline]
56-
OC[OpenClaw]
57-
CX[Codex]
58-
O[Others]
59-
end
60-
61-
CC & CL --> H[Native Hooks]
62-
OC --> S[Shell Shim]
63-
CX --> P[LD_PRELOAD]
64-
O --> M[MCP Proxy]
65-
66-
H & S & P & M --> PE[YAML Policy Eval<br/>~20μs]
67-
68-
PE --> AU[📋 Hash-Chained Audit<br/>Syslog · CEF · Webhooks]
69-
70-
AU --> PASS[✅ Execute]
71-
AU --> BLOCK[❌ Blocked]
72-
AU --> APR[👤 Approval]
73-
74-
PE -. "ambiguous ⚠️" .-> SB["⚡ rampart-verify<br/>(optional sidecar)<br/>gpt-4o-mini · Haiku · Ollama"]
75-
SB -. allow/deny .-> PE
76-
77-
style PE fill:#238636,stroke:#fff,color:#fff
78-
style AU fill:#1f6feb,stroke:#fff,color:#fff
79-
style BLOCK fill:#da3633,stroke:#fff,color:#fff
80-
style APR fill:#d29922,stroke:#fff,color:#fff
81-
style PASS fill:#238636,stroke:#fff,color:#fff
82-
style SB fill:#2d333b,stroke:#f0883e,stroke-width:2px,stroke-dasharray: 5 5
83-
```
47+
<img src="docs/architecture.svg" alt="Rampart architecture — agents flow through interception layer into policy engine, with audit trail and outcomes" width="100%">
8448

8549
*Pattern matching handles 95%+ of decisions in microseconds. The optional [rampart-verify](https://github.com/peg/rampart-verify) sidecar adds LLM-based classification for ambiguous commands. All decisions go to a hash-chained audit trail.*
8650

@@ -419,7 +383,7 @@ graph LR
419383
D -->|Webhook| WH["Signed URL<br/>(click to approve)"]
420384
D -->|CLI / API| CLI["rampart approve &lt;id&gt;"]
421385
422-
CC -->|user responds| R[Resolved]
386+
CC -->|user responds| R[Resolved]
423387
MCP -->|via API / dashboard| R
424388
OC -->|via API| R
425389
WH -->|HMAC-verified link| R

cmd/rampart/cli/cli_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ func TestVersionCommand(t *testing.T) {
3535
assert.Contains(t, stdout, "rampart "+build.Version)
3636
}
3737

38+
func TestVersionFlag(t *testing.T) {
39+
stdout, _, err := runCLI(t, "--version")
40+
require.NoError(t, err)
41+
assert.Contains(t, stdout, "rampart "+build.Version)
42+
}
43+
3844
func TestInitCreatesFile(t *testing.T) {
3945
dir := t.TempDir()
4046
t.Setenv("HOME", dir)

cmd/rampart/cli/doctor.go

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,9 @@ func runDoctor(w io.Writer, jsonOut bool) error {
131131
issues += doctorHooks(emit)
132132

133133
// 7. Audit directory
134-
issues += doctorAudit(emit)
134+
auditIssues, auditWarnings := doctorAudit(emit)
135+
issues += auditIssues
136+
warnings += auditWarnings
135137

136138
// 8. Server running on default port
137139
serverIssues, serveURL := doctorServer(emit)
@@ -457,15 +459,15 @@ func doctorHooks(emit emitFn) int {
457459
if count > 0 {
458460
emit("Hooks", "ok", fmt.Sprintf("Claude Code (%d matchers in settings.json)", count))
459461
} else {
460-
emit("Hooks", "fail", "Claude Code (no Rampart hooks in settings.json)")
462+
emit("Hooks", "fail", fmt.Sprintf("Claude Code hook not installed - expected hook in %s\n Run: rampart setup claude-code", claudeSettingsPath))
461463
issues++
462464
}
463465
} else {
464-
emit("Hooks", "fail", "Claude Code (invalid settings.json)")
466+
emit("Hooks", "fail", fmt.Sprintf("Claude Code settings invalid at %s\n Run: rampart setup claude-code", claudeSettingsPath))
465467
issues++
466468
}
467469
} else {
468-
emit("Hooks", "fail", "Claude Code (no settings.json found)")
470+
emit("Hooks", "fail", fmt.Sprintf("Claude Code hook not installed - expected hook in %s\n Run: rampart setup claude-code", claudeSettingsPath))
469471
issues++
470472
}
471473
}
@@ -484,11 +486,11 @@ func doctorHooks(emit emitFn) int {
484486
if hookCount > 0 {
485487
emit("Hooks", "ok", fmt.Sprintf("Cline (%d hook scripts)", hookCount))
486488
} else {
487-
emit("Hooks", "fail", "Cline (no Rampart hooks found)")
489+
emit("Hooks", "fail", fmt.Sprintf("Cline hook not installed - expected Rampart hook scripts in %s\n Run: rampart setup cline", clineDir))
488490
issues++
489491
}
490492
} else {
491-
emit("Hooks", "fail", "Cline (no Hooks directory found)")
493+
emit("Hooks", "fail", fmt.Sprintf("Cline hook not installed - expected Rampart hook scripts in %s\n Run: rampart setup cline", clineDir))
492494
issues++
493495
}
494496
}
@@ -533,24 +535,24 @@ func countClaudeHookMatchers(settings map[string]any) int {
533535
return count
534536
}
535537

536-
func doctorAudit(emit emitFn) int {
538+
func doctorAudit(emit emitFn) (issues int, warnings int) {
537539
home, err := os.UserHomeDir()
538540
if err != nil {
539-
return 0
541+
return 0, 0
540542
}
541543
auditDir := filepath.Join(home, ".rampart", "audit")
542544

543545
entries, err := os.ReadDir(auditDir)
544546
if err != nil {
545547
emit("Audit", "fail", fmt.Sprintf("%s (not found)", auditDir))
546-
return 1
548+
return 1, 0
547549
}
548550

549551
// Check writable
550552
testFile := filepath.Join(auditDir, ".doctor-write-test")
551553
if err := os.WriteFile(testFile, []byte("test"), 0o600); err != nil {
552554
emit("Audit", "fail", fmt.Sprintf("%s (not writable)", auditDir))
553-
return 1
555+
return 1, 0
554556
}
555557
// Defer removal so the temp file is cleaned up on every exit path,
556558
// including early returns and panics, not just the happy path.
@@ -566,7 +568,7 @@ func doctorAudit(emit emitFn) int {
566568

567569
if len(files) == 0 {
568570
emit("Audit", "ok", fmt.Sprintf("~/%s (0 files)", relHome(auditDir, home)))
569-
return 0
571+
return 0, 0
570572
}
571573

572574
// Find latest modification time
@@ -579,7 +581,33 @@ func doctorAudit(emit emitFn) int {
579581
}
580582

581583
emit("Audit", "ok", fmt.Sprintf("~/%s (%d files, latest: %s)", relHome(auditDir, home), len(files), latest))
582-
return 0
584+
585+
worldReadable := make([]string, 0, 4)
586+
_ = filepath.WalkDir(auditDir, func(path string, d os.DirEntry, walkErr error) error {
587+
if walkErr != nil || d.IsDir() {
588+
return nil
589+
}
590+
info, err := d.Info()
591+
if err != nil {
592+
return nil
593+
}
594+
if info.Mode().Perm()&0o004 != 0 {
595+
if len(worldReadable) < 3 {
596+
worldReadable = append(worldReadable, relHome(path, home))
597+
}
598+
warnings++
599+
}
600+
return nil
601+
})
602+
if warnings > 0 {
603+
sample := strings.Join(worldReadable, ", ")
604+
msg := fmt.Sprintf("%d world-readable audit file(s) found", warnings)
605+
if sample != "" {
606+
msg += fmt.Sprintf(" (e.g. ~/%s)", sample)
607+
}
608+
emit("Audit perms", "warn", msg)
609+
}
610+
return 0, warnings
583611
}
584612

585613
func doctorVersionCheck(w io.Writer, silent bool, emit emitFn) int {

cmd/rampart/cli/doctor_test.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@ import (
1717
"bytes"
1818
"encoding/json"
1919
"errors"
20+
"os"
21+
"path/filepath"
2022
"strings"
2123
"testing"
2224
"time"
2325
)
2426

25-
2627
func TestRunDoctor(t *testing.T) {
2728
var buf bytes.Buffer
2829
err := runDoctor(&buf, false)
@@ -159,3 +160,42 @@ func TestDoctorToken_Missing(t *testing.T) {
159160
// Just verify no panic.
160161
_, _ = doctorToken(emit)
161162
}
163+
164+
func TestDoctorHooks_PathHints(t *testing.T) {
165+
home := t.TempDir()
166+
t.Setenv("HOME", home)
167+
168+
claudeDir := filepath.Join(home, ".claude")
169+
requireNoErr(t, os.MkdirAll(claudeDir, 0o755))
170+
requireNoErr(t, os.WriteFile(filepath.Join(claudeDir, "settings.json"), []byte(`{"hooks":{}}`), 0o644))
171+
172+
clineHooksDir := filepath.Join(home, "Documents", "Cline", "Hooks")
173+
requireNoErr(t, os.MkdirAll(clineHooksDir, 0o755))
174+
175+
var results []checkResult
176+
emit := func(name, status, msg string) {
177+
results = append(results, checkResult{Name: name, Status: status, Message: msg})
178+
}
179+
issues := doctorHooks(emit)
180+
if issues != 2 {
181+
t.Fatalf("expected 2 issues, got %d (%+v)", issues, results)
182+
}
183+
184+
out := ""
185+
for _, r := range results {
186+
out += r.Message + "\n"
187+
}
188+
if !strings.Contains(out, filepath.Join(home, ".claude", "settings.json")) {
189+
t.Fatalf("expected Claude settings path in output, got: %s", out)
190+
}
191+
if !strings.Contains(out, filepath.Join(home, "Documents", "Cline", "Hooks")) {
192+
t.Fatalf("expected Cline hooks path in output, got: %s", out)
193+
}
194+
}
195+
196+
func requireNoErr(t *testing.T, err error) {
197+
t.Helper()
198+
if err != nil {
199+
t.Fatal(err)
200+
}
201+
}

0 commit comments

Comments
 (0)