@haposoft/cafekit 0.13.4 → 0.14.0

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.
Files changed (119) hide show
  1. package/README.md +2 -23
  2. package/bin/lib/i18n.js +2 -2
  3. package/bin/lib/manifest.js +15 -0
  4. package/bin/lib/opencode-install.js +0 -14
  5. package/bin/phases/claude-runtime.js +4 -1
  6. package/bin/phases/claude-settings.js +38 -7
  7. package/bin/phases/root-config.js +5 -24
  8. package/package.json +1 -1
  9. package/src/claude/agents/ui-ux-designer.md +4 -4
  10. package/src/claude/gitignore +4 -11
  11. package/src/claude/hooks/__tests__/privacy-block.test.js +79 -0
  12. package/src/claude/hooks/__tests__/session-writeenv.test.js +68 -0
  13. package/src/claude/hooks/__tests__/spec-gate.test.js +270 -0
  14. package/src/claude/hooks/privacy-block.cjs +22 -2
  15. package/src/claude/hooks/session.cjs +5 -1
  16. package/src/claude/hooks/spec-gate.cjs +173 -0
  17. package/src/claude/hooks/spec-state.cjs +10 -17
  18. package/src/claude/hooks/task-scaffold-guard.cjs +4 -2
  19. package/src/claude/migration-manifest.json +5 -5
  20. package/src/claude/rules/skill-domain-routing.md +2 -3
  21. package/src/claude/rules/skill-workflow-routing.md +2 -11
  22. package/src/claude/rules/state-sync.md +4 -0
  23. package/src/claude/runtime.json +4 -0
  24. package/src/claude/scripts/validate-docs.cjs +4 -7
  25. package/src/claude/scripts/validate-spec-output.cjs +29 -9
  26. package/src/claude/settings/settings.json +4 -0
  27. package/src/claude/skills/ai-multimodal/.env.example +11 -13
  28. package/src/claude/skills/ai-multimodal/SKILL.md +1 -1
  29. package/src/claude/skills/ai-multimodal/references/audio-processing.md +25 -27
  30. package/src/claude/skills/ai-multimodal/references/video-analysis.md +23 -27
  31. package/src/claude/skills/ai-multimodal/references/vision-understanding.md +26 -26
  32. package/src/claude/skills/ai-multimodal/scripts/check_setup.py +1 -1
  33. package/src/claude/skills/ai-multimodal/scripts/gemini_batch_process.py +7 -9
  34. package/src/claude/skills/brainstorm/SKILL.md +1 -2
  35. package/src/claude/skills/docx/SKILL.md +1 -1
  36. package/src/claude/skills/frontend-design/SKILL.md +1 -1
  37. package/src/claude/skills/frontend-design/references/ai-multimodal-overview.md +4 -4
  38. package/src/claude/skills/frontend-design/references/analysis-techniques.md +3 -3
  39. package/src/claude/skills/frontend-design/references/asset-generation.md +2 -2
  40. package/src/claude/skills/frontend-design/references/design-extraction-overview.md +4 -4
  41. package/src/claude/skills/frontend-design/references/extraction-best-practices.md +3 -3
  42. package/src/claude/skills/frontend-design/references/technical-accessibility.md +1 -1
  43. package/src/claude/skills/frontend-design/references/technical-optimization.md +1 -1
  44. package/src/claude/skills/frontend-design/references/technical-overview.md +2 -2
  45. package/src/claude/skills/frontend-design/references/technical-workflows.md +6 -6
  46. package/src/claude/skills/frontend-design/references/visual-analysis-overview.md +3 -3
  47. package/src/claude/skills/inspect/SKILL.md +3 -3
  48. package/src/claude/skills/inspect/references/external-gemini-inspection.md +9 -9
  49. package/src/claude/skills/pdf/SKILL.md +1 -1
  50. package/src/claude/skills/pptx/SKILL.md +1 -1
  51. package/src/claude/skills/specs/SKILL.md +1 -1
  52. package/src/claude/skills/xlsx/SKILL.md +1 -1
  53. package/src/opencode/AGENTS.md +1 -2
  54. package/src/opencode/plugins/session.ts +2 -2
  55. package/src/opencode/plugins/spec-state.ts +244 -0
  56. package/src/opencode/plugins/task-scaffold-guard.ts +161 -0
  57. package/src/claude/scripts/browser-tool.cjs +0 -138
  58. package/src/claude/skills/delegate/SKILL.md +0 -124
  59. package/src/claude/skills/delegate/references/codex-delegation.md +0 -71
  60. package/src/claude/skills/delegate/references/grok-delegation.md +0 -84
  61. package/src/claude/skills/generate-graph/LICENSE +0 -21
  62. package/src/claude/skills/generate-graph/README.md +0 -523
  63. package/src/claude/skills/generate-graph/SKILL.md +0 -429
  64. package/src/claude/skills/generate-graph/agentloop-core.svg +0 -101
  65. package/src/claude/skills/generate-graph/agents/openai.yaml +0 -4
  66. package/src/claude/skills/generate-graph/assets/samples/sample-style1-flat.png +0 -0
  67. package/src/claude/skills/generate-graph/assets/samples/sample-style2-dark.png +0 -0
  68. package/src/claude/skills/generate-graph/assets/samples/sample-style3-blueprint.png +0 -0
  69. package/src/claude/skills/generate-graph/assets/samples/sample-style4-notion.png +0 -0
  70. package/src/claude/skills/generate-graph/assets/samples/sample-style5-glass.png +0 -0
  71. package/src/claude/skills/generate-graph/assets/samples/sample-style6-claude.png +0 -0
  72. package/src/claude/skills/generate-graph/assets/samples/sample-style7-openai.png +0 -0
  73. package/src/claude/skills/generate-graph/fixtures/agent-memory-types-style4.json +0 -181
  74. package/src/claude/skills/generate-graph/fixtures/api-flow-style7.json +0 -40
  75. package/src/claude/skills/generate-graph/fixtures/mem0-style1.json +0 -297
  76. package/src/claude/skills/generate-graph/fixtures/microservices-style3.json +0 -64
  77. package/src/claude/skills/generate-graph/fixtures/multi-agent-style5.json +0 -45
  78. package/src/claude/skills/generate-graph/fixtures/system-architecture-style6.json +0 -48
  79. package/src/claude/skills/generate-graph/fixtures/tool-call-style2.json +0 -182
  80. package/src/claude/skills/generate-graph/package.json +0 -42
  81. package/src/claude/skills/generate-graph/references/icons.md +0 -281
  82. package/src/claude/skills/generate-graph/references/style-1-flat-icon.md +0 -108
  83. package/src/claude/skills/generate-graph/references/style-2-dark-terminal.md +0 -107
  84. package/src/claude/skills/generate-graph/references/style-3-blueprint.md +0 -113
  85. package/src/claude/skills/generate-graph/references/style-4-notion-clean.md +0 -94
  86. package/src/claude/skills/generate-graph/references/style-5-glassmorphism.md +0 -125
  87. package/src/claude/skills/generate-graph/references/style-6-claude-official.md +0 -209
  88. package/src/claude/skills/generate-graph/references/style-7-openai.md +0 -215
  89. package/src/claude/skills/generate-graph/references/style-diagram-matrix.md +0 -135
  90. package/src/claude/skills/generate-graph/references/svg-layout-best-practices.md +0 -100
  91. package/src/claude/skills/generate-graph/scripts/generate-diagram.sh +0 -157
  92. package/src/claude/skills/generate-graph/scripts/generate-from-template.py +0 -1556
  93. package/src/claude/skills/generate-graph/scripts/test-all-styles.sh +0 -135
  94. package/src/claude/skills/generate-graph/scripts/validate-svg.sh +0 -292
  95. package/src/claude/skills/generate-graph/templates/agent-architecture.svg +0 -28
  96. package/src/claude/skills/generate-graph/templates/architecture.svg +0 -23
  97. package/src/claude/skills/generate-graph/templates/comparison-matrix.svg +0 -14
  98. package/src/claude/skills/generate-graph/templates/data-flow.svg +0 -28
  99. package/src/claude/skills/generate-graph/templates/er-diagram.svg +0 -21
  100. package/src/claude/skills/generate-graph/templates/flowchart.svg +0 -21
  101. package/src/claude/skills/generate-graph/templates/sequence.svg +0 -20
  102. package/src/claude/skills/generate-graph/templates/state-machine.svg +0 -20
  103. package/src/claude/skills/generate-graph/templates/timeline.svg +0 -19
  104. package/src/claude/skills/generate-graph/templates/use-case.svg +0 -21
  105. package/src/claude/skills/impact-analysis/SKILL.md +0 -276
  106. package/src/claude/skills/impact-analysis/references/change-detection.md +0 -270
  107. package/src/claude/skills/impact-analysis/references/dependency-scouting.md +0 -338
  108. package/src/claude/skills/impact-analysis/references/edge-case-identification.md +0 -439
  109. package/src/claude/skills/impact-analysis/references/industry-techniques.md +0 -695
  110. package/src/claude/skills/impact-analysis/references/practical-techniques-guide.md +0 -753
  111. package/src/claude/skills/impact-analysis/references/project-detection.md +0 -704
  112. package/src/claude/skills/impact-analysis/references/react-native-customization.md +0 -508
  113. package/src/claude/skills/impact-analysis/references/report-template.md +0 -604
  114. package/src/claude/skills/impact-analysis/references/test-scenario-generation.md +0 -459
  115. package/src/claude/skills/impact-analysis/scripts/README.md +0 -476
  116. package/src/claude/skills/impact-analysis/scripts/ast-analyze.js +0 -403
  117. package/src/claude/skills/impact-analysis/scripts/calculate-risk.js +0 -475
  118. package/src/claude/skills/impact-analysis/scripts/find-dependencies.sh +0 -202
  119. package/src/claude/skills/impact-analysis/scripts/run-analysis.sh +0 -312
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Claude Code-first spec-driven workflow and runtime bundle for AI coding assistants.
4
4
 
5
- [![Version](https://img.shields.io/badge/version-0.13.4-blue.svg)](https://github.com/haposoft/cafekit)
5
+ [![Version](https://img.shields.io/badge/version-0.9.3-blue.svg)](https://github.com/haposoft/cafekit)
6
6
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
7
7
  [![Claude%20Code](https://img.shields.io/badge/Claude%20Code-Primary-orange.svg)](https://claude.ai/code)
8
8
 
@@ -46,26 +46,6 @@ Requirements:
46
46
  - Node.js 18+
47
47
  - Claude Code project with `.claude/`, OpenCode project with `.opencode/` or `opencode.json`, or choose a runtime when prompted
48
48
 
49
- ## Git ignore policy
50
-
51
- On install, CafeKit updates the **project-root** `.gitignore` with:
52
-
53
- ```text
54
- # CafeKit / Ecosystem
55
- specs/_shared/
56
- plans/
57
- !plans/templates/
58
- .cafekit-backup/
59
- .cafekit.lock
60
- .claude/
61
- .opencode/
62
- ```
63
-
64
- Runtime folders are local — reinstall with `npx @haposoft/cafekit` rather than
65
- committing them. Inside the runtime, CafeKit also installs `.claude/.gitignore`
66
- (or `.opencode/.gitignore`) so secrets, skill dependencies, session state, and
67
- hook logs stay out of git even if someone force-adds the folder.
68
-
69
49
  ## What Gets Installed
70
50
 
71
51
  Claude Code targets:
@@ -131,9 +111,8 @@ CafeKit ships many skills, but the main release surface is:
131
111
  - `/hapo:docs [--init|--update|--summarize|--reconstruct]`: create project docs or reconstruct as-is system documentation from source code
132
112
  - `/hapo:test [scope|--full]`: run verification and return a structured verdict
133
113
  - `/hapo:code-review [scope|--pending]`: adversarial review focused on correctness, regressions, and security
134
- - `/hapo:generate-graph <diagram request>`: generate technical SVG/PNG diagrams
135
114
 
136
- Common companion skills bundled in this package include `inspect`, `impact-analysis`, `research`, `ai-multimodal`, `frontend-development`, `backend-development`, and `react-best-practices`.
115
+ Common companion skills bundled in this package include `inspect`, `research`, `ai-multimodal`, `frontend-development`, `backend-development`, and `react-best-practices`.
137
116
 
138
117
  CafeKit uses rule-based skill routing guidance instead of an automatic prompt-scoring hook. See `.claude/rules/skill-workflow-routing.md`, `.claude/rules/skill-domain-routing.md`, or run:
139
118
 
package/bin/lib/i18n.js CHANGED
@@ -366,11 +366,11 @@ const MESSAGES = {
366
366
  }
367
367
  };
368
368
 
369
- /** Normalize a language code to a supported one; unknown → 'ja'. */
369
+ /** Normalize a language code to a supported one; unknown → 'en'. */
370
370
  function resolveLang(code) {
371
371
  if (!code) return 'en';
372
372
  const lc = String(code).toLowerCase().slice(0, 2);
373
- return SUPPORTED.includes(lc) ? lc : 'ja';
373
+ return SUPPORTED.includes(lc) ? lc : 'en';
374
374
  }
375
375
 
376
376
  /** Build a translator. Falls back to English per-key, then the key itself. */
@@ -105,6 +105,21 @@ function createTracker(platformFolder, version) {
105
105
  this._pruned.add(key);
106
106
  },
107
107
 
108
+ /**
109
+ * Prune every tracked key under a directory prefix (e.g. "skills/foo/").
110
+ * Marks prior-manifest keys with the same prefix for deletion on write().
111
+ */
112
+ prunePrefix(relPrefix) {
113
+ const prefix = relPrefix.replace(/\\/g, '/');
114
+ for (const key of Object.keys(files)) {
115
+ if (key.startsWith(prefix)) delete files[key];
116
+ }
117
+ const prior = read(platformFolder);
118
+ for (const key of Object.keys(prior.files || {})) {
119
+ if (key.startsWith(prefix)) this._pruned.add(key);
120
+ }
121
+ },
122
+
108
123
  /** Recorded hash for a relPath written earlier this run, or null. */
109
124
  recorded(rel) {
110
125
  const key = rel.replace(/\\/g, '/');
@@ -94,13 +94,6 @@ const OPENCODE_COMMAND_TEMPLATES = [
94
94
  subtask: true,
95
95
  description: 'Inspect a codebase, artifact, or external context before planning.'
96
96
  },
97
- {
98
- fileName: 'generate-graph.md',
99
- skillName: 'generate-graph',
100
- agent: 'god-developer',
101
- subtask: true,
102
- description: 'Generate technical diagrams from project context.'
103
- },
104
97
  // Domain skills — wrappers so users can invoke them directly via slash
105
98
  // commands. Each delegates to the matching skill under .opencode/skills/.
106
99
  {
@@ -166,13 +159,6 @@ const OPENCODE_COMMAND_TEMPLATES = [
166
159
  subtask: true,
167
160
  description: 'Run git workflows: branch, commit, tag, push, or release prep.'
168
161
  },
169
- {
170
- fileName: 'impact-analysis.md',
171
- skillName: 'impact-analysis',
172
- agent: 'inspector',
173
- subtask: true,
174
- description: 'Assess affected files, blast radius, and side effects of a change.'
175
- },
176
162
  {
177
163
  fileName: 'mobile-development.md',
178
164
  skillName: 'mobile-development',
@@ -78,11 +78,14 @@ function removeObsoleteClaudeRuntimeFiles(ctx, platformKey) {
78
78
  const targetPath = path.join(targetBase, relPath);
79
79
  if (!fs.existsSync(targetPath)) return;
80
80
 
81
+ const isDir = fs.statSync(targetPath).isDirectory();
81
82
  if (!ctx.dryRun) {
82
- fs.rmSync(targetPath, { force: true });
83
+ fs.rmSync(targetPath, { force: true, recursive: isDir });
83
84
  // Remove the entry from the manifest so it doesn't linger as a zombie.
84
85
  if (ctx.trackers && ctx.trackers.claude) {
85
86
  ctx.trackers.claude.prune(relPath);
87
+ // Directories: prune every tracked file underneath as well.
88
+ if (isDir) ctx.trackers.claude.prunePrefix(`${relPath}/`);
86
89
  }
87
90
  }
88
91
  ctx.ui.detail(` ↻ ${ctx.dryRun ? '[dry-run] ' : ''}Removed obsolete runtime: ${relPath}`);
@@ -85,7 +85,16 @@ function mergeClaudeSettings(ctx, platformKey) {
85
85
  const managedSettings = JSON.parse(fs.readFileSync(templatePath, 'utf8'));
86
86
  let existingSettings = {};
87
87
  if (fs.existsSync(targetPath)) {
88
- existingSettings = JSON.parse(fs.readFileSync(targetPath, 'utf8'));
88
+ // A malformed user settings.json must not abort the whole install: skip the
89
+ // settings merge (never overwrite the user's file blind) and tell them why.
90
+ try {
91
+ existingSettings = JSON.parse(fs.readFileSync(targetPath, 'utf8'));
92
+ } catch (e) {
93
+ ctx.ui.warn(`Settings: ${targetPath} is not valid JSON (${e.message}). ` +
94
+ 'Skipping settings merge — fix the file and re-run the installer.');
95
+ ctx.results.errors++;
96
+ return;
97
+ }
89
98
  }
90
99
 
91
100
  const mergedSettings = pruneObsoleteSettingsHooks({ ...existingSettings }, ctx);
@@ -100,7 +109,10 @@ function mergeClaudeSettings(ctx, platformKey) {
100
109
  }
101
110
  }
102
111
 
103
- // hooks: append managed hooks that aren't already present (dedupe by command).
112
+ // hooks: merge managed hooks per command (not per entry). An entry is keyed
113
+ // by its matcher; commands missing from the matching entry are appended, so
114
+ // a new command added to an existing matcher entry is not swallowed on
115
+ // upgrade (the old dedupe only looked at each entry's FIRST command).
104
116
  if (managedSettings.hooks) {
105
117
  mergedSettings.hooks = mergedSettings.hooks || {};
106
118
  Object.keys(managedSettings.hooks).forEach((eventName) => {
@@ -108,14 +120,33 @@ function mergeClaudeSettings(ctx, platformKey) {
108
120
  const existingHooks = mergedSettings.hooks[eventName] || [];
109
121
  const mergedHooks = [...existingHooks];
110
122
 
123
+ // Commands already registered anywhere under this event (any matcher):
124
+ // a user may have moved a hook to another entry — never re-add it.
125
+ const eventCommands = new Set(
126
+ mergedHooks.flatMap((entry) => (entry?.hooks || []).map((h) => h?.command)).filter(Boolean)
127
+ );
128
+
111
129
  managedHooks.forEach((managedHook) => {
112
- const managedCommand = managedHook.hooks?.[0]?.command || '';
113
- const isDuplicate = mergedHooks.some((existingHook) =>
114
- existingHook.hooks?.some((h) => h.command === managedCommand));
115
- if (!isDuplicate) {
116
- mergedHooks.push(managedHook);
130
+ const managedMatcher = managedHook.matcher || '';
131
+ const managedCommands = (managedHook.hooks || [])
132
+ .filter((h) => h?.command && !eventCommands.has(h.command));
133
+ if (managedCommands.length === 0) return;
134
+
135
+ const target = mergedHooks.find((entry) =>
136
+ (entry?.matcher || '') === managedMatcher && Array.isArray(entry?.hooks));
137
+
138
+ if (!target) {
139
+ mergedHooks.push({ ...managedHook, hooks: managedCommands });
140
+ managedCommands.forEach((h) => eventCommands.add(h.command));
117
141
  ctx.ui.detail(` ✓ ${ctx.dryRun ? '[dry-run] ' : ''}Settings: hook ${eventName} merged`);
142
+ return;
118
143
  }
144
+
145
+ managedCommands.forEach((hook) => {
146
+ target.hooks.push(hook);
147
+ eventCommands.add(hook.command);
148
+ });
149
+ ctx.ui.detail(` ✓ ${ctx.dryRun ? '[dry-run] ' : ''}Settings: hook ${eventName} merged (+${managedCommands.length} command${managedCommands.length > 1 ? 's' : ''})`);
119
150
  });
120
151
 
121
152
  mergedSettings.hooks[eventName] = mergedHooks;
@@ -1,29 +1,13 @@
1
1
  /**
2
2
  * Phase: root project configuration (.gitignore).
3
3
  *
4
- * Ensures the root .gitignore carries CafeKit-managed patterns:
5
- * - workflow dirs (plans, shared research)
6
- * - install safety artifacts (backup, lock)
7
- * - runtime folders (.claude/, .opencode/) — reinstall via
8
- * `npx @haposoft/cafekit`; do not commit the local payload
9
- *
10
- * Layered with the in-folder `.claude/.gitignore` /
11
- * `.opencode/.gitignore` templates (secrets, skill deps, session
12
- * state) so force-adds and partial un-ignores stay safe.
13
- *
14
- * Honors dry-run.
4
+ * Ensures the root .gitignore carries CafeKit-managed patterns, including the
5
+ * new install backup dir and lock file so they never get committed. Honors dry-run.
15
6
  */
16
7
 
17
8
  const fs = require('fs');
18
9
  const path = require('path');
19
10
 
20
- /** True if `lines` already carries `pattern` with or without a trailing slash. */
21
- function hasPattern(lines, pattern) {
22
- const bare = pattern.replace(/\/$/, '');
23
- const withSlash = bare + '/';
24
- return lines.includes(pattern) || lines.includes(bare) || lines.includes(withSlash);
25
- }
26
-
27
11
  function ensureGitignore(ctx) {
28
12
  const gitignorePath = path.join(process.cwd(), '.gitignore');
29
13
  const header = '# CafeKit / Ecosystem';
@@ -32,10 +16,7 @@ function ensureGitignore(ctx) {
32
16
  'plans/',
33
17
  '!plans/templates/',
34
18
  '.cafekit-backup/',
35
- '.cafekit.lock',
36
- // Local runtime payload — reinstall with npx; keep out of git
37
- '.claude/',
38
- '.opencode/'
19
+ '.cafekit.lock'
39
20
  ];
40
21
  const prefix = ctx.dryRun ? '[dry-run] ' : '';
41
22
 
@@ -49,7 +30,7 @@ function ensureGitignore(ctx) {
49
30
 
50
31
  const content = fs.readFileSync(gitignorePath, 'utf8');
51
32
  const lines = content.split('\n').map((l) => l.trim());
52
- const missing = patterns.filter((p) => !hasPattern(lines, p));
33
+ const missing = patterns.filter((p) => !lines.includes(p));
53
34
 
54
35
  if (missing.length > 0) {
55
36
  let newContent = content;
@@ -65,4 +46,4 @@ function ensureGitignore(ctx) {
65
46
  }
66
47
  }
67
48
 
68
- module.exports = { ensureGitignore, hasPattern };
49
+ module.exports = { ensureGitignore };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haposoft/cafekit",
3
- "version": "0.13.4",
3
+ "version": "0.14.0",
4
4
  "description": "Claude Code-first spec-driven workflow for AI coding assistants. Bundles CafeKit hapo: skills, runtime hooks, agents, and installer scaffolding.",
5
5
  "author": "Haposoft <nghialt@haposoft.com>",
6
6
  "license": "MIT",
@@ -25,10 +25,10 @@ You are an award-caliber UI/UX designer. You merge aesthetic excellence with eng
25
25
  ### Phase 1: Research & Trend Scouting
26
26
  - Execute explicit searches using the bundled `ui-ux-pro-max` toolkit:
27
27
  ```bash
28
- python3 packages/spec/src/claude/skills/ui-ux-pro-max/scripts/search.py "<product-type>" --domain product
29
- python3 packages/spec/src/claude/skills/ui-ux-pro-max/scripts/search.py "<style-keywords>" --domain style
30
- python3 packages/spec/src/claude/skills/ui-ux-pro-max/scripts/search.py "<mood>" --domain typography
31
- python3 packages/spec/src/claude/skills/ui-ux-pro-max/scripts/search.py "<industry>" --domain color
28
+ .claude/skills/.venv/bin/python3 .claude/skills/ui-ux-pro-max/scripts/search.py "<product-type>" --domain product
29
+ .claude/skills/.venv/bin/python3 .claude/skills/ui-ux-pro-max/scripts/search.py "<style-keywords>" --domain style
30
+ .claude/skills/.venv/bin/python3 .claude/skills/ui-ux-pro-max/scripts/search.py "<mood>" --domain typography
31
+ .claude/skills/.venv/bin/python3 .claude/skills/ui-ux-pro-max/scripts/search.py "<industry>" --domain color
32
32
  ```
33
33
  - Study current design trends sourced from Dribbble, Awwwards, Mobbin via the python extractor outputs.
34
34
  - Review existing `docs/design-guidelines.md` if it exists.
@@ -1,26 +1,19 @@
1
- # CafeKit local runtime (.claude/ or .opencode/)
2
- # Also ignored at project root by the installer. This file protects
3
- # force-adds (`git add -f`) and partial un-ignores of the runtime folder.
4
-
5
- # Secrets and local credentials
1
+ # Environment and Sandbox credentials within .claude
6
2
  .env
7
3
  .env.*
8
4
  !.env.example
9
5
 
10
- # Skill dependency installs (Python venv + npm)
6
+ # Virtual environments for python scripts (skills)
11
7
  skills/.venv/
12
- skills/**/.venv/
13
- skills/**/node_modules/
14
8
  .venv/
15
9
  venv/
10
+ skills/**/node_modules/
16
11
  __pycache__/
17
- **/__pycache__/
18
12
  *.pyc
19
13
 
20
- # Generated session state, caches, and logs
14
+ # System generated state, caches, and logs
21
15
  session-state/
22
16
  hooks/.logs/
23
- plugins/.logs/
24
17
  agent-memory/
25
18
  settings.bak.json
26
19
  .mcp.json
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ // Behavioral tests for privacy-block.cjs. The hook is run as a real subprocess:
4
+ // a PreToolUse payload is piped to stdin and the exit code is asserted
5
+ // (0 = allow, 2 = block). Covers the symlink-bypass hardening.
6
+
7
+ const { test } = require('node:test');
8
+ const assert = require('node:assert');
9
+ const { spawnSync } = require('node:child_process');
10
+ const fs = require('node:fs');
11
+ const os = require('node:os');
12
+ const path = require('node:path');
13
+
14
+ const HOOK = path.join(__dirname, '..', 'privacy-block.cjs');
15
+
16
+ /** Run the hook with a tool payload; return { code, stderr }. */
17
+ function runHook(payload, cwd) {
18
+ const res = spawnSync(process.execPath, [HOOK], {
19
+ input: JSON.stringify({ ...payload, cwd: cwd || payload.cwd }),
20
+ encoding: 'utf8',
21
+ });
22
+ return { code: res.status, stderr: res.stderr || '' };
23
+ }
24
+
25
+ function tmpDir() {
26
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'ck-privacy-'));
27
+ }
28
+
29
+ test('blocks a direct Read of .env (exit 2)', () => {
30
+ const dir = tmpDir();
31
+ try {
32
+ const env = path.join(dir, '.env');
33
+ fs.writeFileSync(env, 'SECRET=1');
34
+ const { code } = runHook({ tool_name: 'Read', tool_input: { file_path: env } }, dir);
35
+ assert.strictEqual(code, 2);
36
+ } finally {
37
+ fs.rmSync(dir, { recursive: true, force: true });
38
+ }
39
+ });
40
+
41
+ test('blocks a symlink whose target is .env (regression: symlink bypass)', () => {
42
+ const dir = tmpDir();
43
+ try {
44
+ const env = path.join(dir, '.env');
45
+ fs.writeFileSync(env, 'SECRET=1');
46
+ const link = path.join(dir, 'notes.txt'); // harmless-looking name
47
+ fs.symlinkSync(env, link);
48
+ const { code } = runHook({ tool_name: 'Read', tool_input: { file_path: link } }, dir);
49
+ assert.strictEqual(code, 2, 'symlink to .env must be blocked, not allowed');
50
+ } finally {
51
+ fs.rmSync(dir, { recursive: true, force: true });
52
+ }
53
+ });
54
+
55
+ test('allows a normal non-sensitive file (exit 0)', () => {
56
+ const dir = tmpDir();
57
+ try {
58
+ const f = path.join(dir, 'README.md');
59
+ fs.writeFileSync(f, '# hi');
60
+ const { code } = runHook({ tool_name: 'Read', tool_input: { file_path: f } }, dir);
61
+ assert.strictEqual(code, 0);
62
+ } finally {
63
+ fs.rmSync(dir, { recursive: true, force: true });
64
+ }
65
+ });
66
+
67
+ test('allows a symlink to an exempt .env.example (exemption still wins)', () => {
68
+ const dir = tmpDir();
69
+ try {
70
+ const example = path.join(dir, '.env.example');
71
+ fs.writeFileSync(example, 'SECRET=');
72
+ const link = path.join(dir, 'sample.txt');
73
+ fs.symlinkSync(example, link);
74
+ const { code } = runHook({ tool_name: 'Read', tool_input: { file_path: link } }, dir);
75
+ assert.strictEqual(code, 0, 'symlink to an exempt file must stay allowed');
76
+ } finally {
77
+ fs.rmSync(dir, { recursive: true, force: true });
78
+ }
79
+ });
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ // Behavioral test for session.cjs writeEnv escaping. A git branch name is an
4
+ // attacker-influenced value that flows into GIT_BRANCH in CLAUDE_ENV_FILE. If
5
+ // the value is not escaped, sourcing that env file expands `$HOME` and executes
6
+ // the `pwd` command substitution. We build a repo whose branch carries both
7
+ // payloads (space-free, since git bans spaces in refs), run the hook, then
8
+ // source the produced env file and assert GIT_BRANCH read back is the exact
9
+ // literal branch name — nothing expanded or executed.
10
+
11
+ const { test } = require('node:test');
12
+ const assert = require('node:assert');
13
+ const { spawnSync } = require('node:child_process');
14
+ const fs = require('node:fs');
15
+ const os = require('node:os');
16
+ const path = require('node:path');
17
+
18
+ const HOOK = path.join(__dirname, '..', 'session.cjs');
19
+
20
+ function git(args, cwd) {
21
+ const res = spawnSync('git', args, { cwd, encoding: 'utf8' });
22
+ if (res.status !== 0) {
23
+ throw new Error(`git ${args.join(' ')} failed: ${res.stderr}`);
24
+ }
25
+ return res.stdout.trim();
26
+ }
27
+
28
+ test('session.cjs escapes shell metacharacters in GIT_BRANCH (no injection when sourced)', () => {
29
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-session-'));
30
+ const envFile = path.join(dir, 'env.sh');
31
+ try {
32
+ // Branch name carries both a $HOME expansion and a `pwd` command
33
+ // substitution. Both are space-free so git accepts them as a valid ref.
34
+ const payloadBranch = 'evil$HOME-x`pwd`';
35
+ git(['init', '-q'], dir);
36
+ git(['config', 'user.email', 't@t.co'], dir);
37
+ git(['config', 'user.name', 't'], dir);
38
+ git(['commit', '-q', '--allow-empty', '-m', 'init'], dir);
39
+ git(['checkout', '-q', '-b', payloadBranch], dir);
40
+
41
+ // Run the hook exactly as Claude Code would (SessionStart on stdin).
42
+ const res = spawnSync(process.execPath, [HOOK], {
43
+ cwd: dir,
44
+ input: JSON.stringify({ source: 'startup' }),
45
+ encoding: 'utf8',
46
+ env: { ...process.env, CLAUDE_ENV_FILE: envFile },
47
+ });
48
+ assert.strictEqual(res.status, 0, 'hook must exit 0 (fail-open)');
49
+ assert.ok(fs.existsSync(envFile), 'env file must be written');
50
+
51
+ const envText = fs.readFileSync(envFile, 'utf8');
52
+ const branchLine = envText.split('\n').find((l) => l.startsWith('export GIT_BRANCH='));
53
+ assert.ok(branchLine, 'GIT_BRANCH must be present');
54
+ assert.ok(branchLine.includes('\\$') && branchLine.includes('\\`'), 'both $ and backtick must be escaped');
55
+
56
+ // The real proof: source the env file, then echo the value back. It must
57
+ // equal the literal branch name — no $HOME expansion, no pwd execution.
58
+ const sourced = spawnSync('sh', ['-c', `. "${envFile}" && printf '%s' "$GIT_BRANCH"`], { encoding: 'utf8' });
59
+ assert.strictEqual(sourced.status, 0, 'sourcing env file should succeed');
60
+ assert.strictEqual(
61
+ sourced.stdout,
62
+ payloadBranch,
63
+ 'GIT_BRANCH must read back as the exact literal branch (no expansion/execution)',
64
+ );
65
+ } finally {
66
+ fs.rmSync(dir, { recursive: true, force: true });
67
+ }
68
+ });