claude-toolkit 0.10.0 → 0.15.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 (40) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +2 -0
  3. package/core/agents/ct-code-reviewer.md +5 -4
  4. package/core/agents/ct-github-workflow.md +28 -15
  5. package/core/commands/ct/code-quality.md +22 -2
  6. package/core/commands/ct/onboard.md +8 -3
  7. package/core/commands/ct/pr-review.md +22 -88
  8. package/core/commands/ct/pr-summary.md +6 -3
  9. package/core/commands/ct/ticket.md +20 -3
  10. package/core/hooks/{skill-eval.js → skill-eval.cjs} +103 -41
  11. package/core/hooks/skill-rules.schema.json +4 -0
  12. package/core/skills/ct-code-style/SKILL.md +134 -0
  13. package/core/skills/ct-systematic-debugging/SKILL.md +1 -1
  14. package/core/skills/ct-testing-patterns/SKILL.md +5 -7
  15. package/core/skills/ct-typescript-conventions/SKILL.md +15 -11
  16. package/core/skills/ct-verification-before-completion/SKILL.md +2 -1
  17. package/docs/README.md +2 -0
  18. package/docs/commands/code-quality.md +2 -8
  19. package/docs/commands/onboard.md +2 -11
  20. package/docs/commands/pr-review.md +2 -13
  21. package/docs/commands/pr-summary.md +2 -2
  22. package/docs/commands/proto-check.md +4 -4
  23. package/docs/commands/ticket.md +2 -2
  24. package/docs/skills/code-style.md +26 -0
  25. package/docs/skills/systematic-debugging.md +1 -1
  26. package/docs/skills/testing-patterns.md +78 -56
  27. package/docs/skills/typescript-conventions.md +23 -10
  28. package/docs/skills/verification-before-completion.md +1 -1
  29. package/docs/stacks/esnext-idioms.md +25 -0
  30. package/package.json +2 -1
  31. package/src/detect.ts +74 -0
  32. package/src/generator.ts +19 -2
  33. package/src/types.ts +1 -0
  34. package/stacks/esnext/skills/ct-esnext-idioms/SKILL.md +130 -0
  35. package/stacks/esnext/stack.json +37 -0
  36. package/{core/commands/ct → stacks/protobuf/commands}/proto-check.md +11 -2
  37. package/stacks/solidjs/skills/ct-solidjs-patterns/SKILL.md +1 -1
  38. package/templates/configs/biome.base.json +8 -1
  39. package/templates/skill-rules.base.json +18 -23
  40. package/core/hooks/skill-eval.sh +0 -35
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.0 (2026-07-04)
4
+
5
+ Two new authoring-convention assets — an opt-in `esnext` stack and an always-on `ct-code-style` core skill — plus Biome enforcement of the mechanically-checkable style rules.
6
+
7
+ - feat: add the `esnext` stack (skill `ct-esnext-idioms`) — availability-aware guidance on modern ECMAScript runtime features: `Temporal` over `Date`, `using`/`await using` for deterministic cleanup, lazy iterator helpers, `structuredClone`, `Object.groupBy`/`Map.groupBy`, `Promise.withResolvers`, immutable array ops, and ESM hygiene. Detected on tsconfig `target`/`module`/`lib` = ESNext/ES2022+, package.json `"type":"module"`, `engines.node >= 20`, or a `.mjs` file.
8
+ - feat: add the `ct-code-style` core skill (always included) — universal code-structure conventions: guard clauses over nested `else`, object lookup tables over `if/else if` chains, single-level ternaries, full ES6 destructuring, and arrow-only functions with no `class`/`this`. Carve-outs defer to stack skills: never destructure reactive SolidJS `props`, and use `class`/`this` only where a platform mandates it (e.g. Cloudflare Durable Objects).
9
+ - feat: enforce the mechanically-checkable style rules in the scaffolded Biome config — `noUselessElse`, `noNestedTernary`, and `useArrowFunction`. `noMagicNumbers` is kept as skill guidance only (too noisy to enforce).
10
+ - docs: document both assets in the README, `docs/stacks/esnext-idioms.md`, `docs/skills/code-style.md`, and the docs index; cross-link the SolidJS props rule to `ct-code-style`.
11
+
12
+ ## 0.11.0 (2026-07-04)
13
+
14
+ Remediation of the 2026-07-04 core audit (46 findings): the skill-eval hook is hardened, stack-owned commands are gated behind their stack, and the `ct-` agents, commands, skills, and reference docs are corrected and aligned.
15
+
16
+ - feat: gate stack commands behind their owning stack — `proto-check` moved to the protobuf stack and is only installed when protobuf is detected. The skill hook is now registered as a single command string rather than exec-form args, so it runs correctly on every prompt.
17
+ - fix(hooks): harden the skill-eval hook — correct glob-to-regex conversion, robust file-path extraction with an input cap, defensive config coercion with defaults and clean exit, output sanitization, and verification that referenced skills actually ship. Dropped ghost rules and locked the skill-rules schema with `additionalProperties: false`. Renamed to `.cjs` and exposed a testable module API.
18
+ - fix(agents): register `ct-code-reviewer` and `ct-github-workflow` under their `ct-` names with scoped tool grants; give `ct-github-workflow` a real merge-conflict probe and default-branch resolution.
19
+ - fix(commands): scope `Bash` grants and add `argument-hint` across `code-quality`, `onboard`, `ticket`, and `pr-summary`; `pr-review` now fetches the PR diff and delegates to `ct-code-reviewer`.
20
+ - fix(skills): correct the `satisfies` example, align type-vs-interface guidance with best-practices, narrow E2E scope, and add edge cases plus "Use-when" triggers to the debugging and verification skills.
21
+ - docs: add `CLAUDE.md` architecture guide, regenerate testing-patterns from the current skill, correct command invocation names to `/ct:`, and mirror the doc references to the moved/renamed assets.
22
+ - chore: add `.gitattributes` to normalize line endings to LF across platforms.
23
+
3
24
  ## 0.10.0 (2026-06-09)
4
25
 
5
26
  One idempotent CLI command replaces `init`/`update`/`sync`, and `.claude/` now regenerates itself on toolkit upgrade.
package/README.md CHANGED
@@ -107,6 +107,7 @@ Stacks in your config that are no longer detected are reported but left unchange
107
107
  | `playwright` | Playwright E2E testing, Page Objects, fixtures, CI/CD |
108
108
  | `storybook` | Storybook interaction testing, CSF 3, visual regression |
109
109
  | `capacitor` | Capacitor 8 runtime, Capgo OTA, channels; webview UI & native feel |
110
+ | `esnext` | Modern ECMAScript idioms — Temporal, iterator helpers, `using`, structuredClone |
110
111
 
111
112
  ## Core Features (always included)
112
113
 
@@ -122,6 +123,7 @@ Stacks in your config that are no longer detected are reported but left unchange
122
123
  - `ct-testing-patterns` — TDD workflow and patterns
123
124
  - `ct-typescript-conventions` — TypeScript strict mode best practices
124
125
  - `ct-verification-before-completion` — Evidence-based completion claims
126
+ - `ct-code-style` — Code structure & style: guard clauses, lookup tables, no magic values
125
127
 
126
128
  ### Core Commands
127
129
 
@@ -1,7 +1,8 @@
1
1
  ---
2
- name: "Code Reviewer"
3
- description: "Senior code reviewer with stack-aware analysis"
2
+ name: ct-code-reviewer
3
+ description: Senior code reviewer with stack-aware analysis
4
4
  model: opus
5
+ tools: Read, Grep, Glob, Bash
5
6
  ---
6
7
 
7
8
  # Code Reviewer Agent
@@ -75,7 +76,7 @@ You are a senior code reviewer. Your job is to review code changes thoroughly, p
75
76
 
76
77
  ## Output Format
77
78
 
78
- ```markdown
79
+ ````markdown
79
80
  ## Code Review
80
81
 
81
82
  **Reviewed:** {files or PR description}
@@ -112,7 +113,7 @@ You are a senior code reviewer. Your job is to review code changes thoroughly, p
112
113
 
113
114
  ### Summary
114
115
  {1-3 sentence overall assessment. Be direct.}
115
- ```
116
+ ````
116
117
 
117
118
  ## Principles
118
119
 
@@ -1,6 +1,6 @@
1
1
  ---
2
- name: "GitHub Workflow"
3
- description: "Git workflow assistant for branching, commits, and PRs"
2
+ name: ct-github-workflow
3
+ description: Git workflow assistant for branching, commits, and PRs
4
4
  model: sonnet
5
5
  ---
6
6
 
@@ -64,18 +64,27 @@ perf(search): add database index for full-text queries
64
64
  ## PR Creation Workflow
65
65
 
66
66
  ### 1. Pre-flight Checks
67
- ```bash
68
- # Verify tests pass
69
- {project test command}
70
67
 
71
- # Verify types check
72
- {project typecheck command}
68
+ First run the project's own verification commands (substitute this project's actual scripts — these are placeholders, not literal commands):
69
+ - **Tests** — run the project's test command
70
+ - **Types** — run the project's typecheck command
71
+ - **Lint** — run the project's lint command
73
72
 
74
- # Verify no lint errors
75
- {project lint command}
73
+ Then confirm the branch is current with, and conflict-free against, the default branch:
76
74
 
77
- # Check for merge conflicts
78
- git fetch origin main && git merge-base --is-ancestor origin/main HEAD
75
+ ```bash
76
+ # Resolve the default branch (main, master, ...) instead of assuming main
77
+ BASE=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's@^origin/@@')
78
+ BASE=${BASE:-main}
79
+ git fetch origin "$BASE"
80
+
81
+ # Up-to-date check: a non-zero exit means the base has commits not yet on this
82
+ # branch (i.e. the branch is behind) — this is NOT a conflict signal.
83
+ git merge-base --is-ancestor "origin/$BASE" HEAD
84
+
85
+ # Real merge-conflict probe (does not touch the working tree): a non-zero exit
86
+ # means merging the base into this branch would produce conflicts.
87
+ git merge-tree --write-tree "origin/$BASE" HEAD >/dev/null
79
88
  ```
80
89
 
81
90
  ### 2. PR Title
@@ -114,8 +123,12 @@ Keep under 70 characters. The title should tell a reviewer what this PR does wit
114
123
  ## Workflow Commands
115
124
 
116
125
  ```bash
117
- # Create feature branch from latest main
118
- git checkout main && git pull && git checkout -b feat/description
126
+ # Resolve the default branch once (main, master, ...)
127
+ BASE=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's@^origin/@@')
128
+ BASE=${BASE:-main}
129
+
130
+ # Create feature branch from the latest default branch
131
+ git checkout "$BASE" && git pull && git checkout -b feat/description
119
132
 
120
133
  # Stage and commit with conventional message
121
134
  git add {files}
@@ -125,8 +138,8 @@ git commit -m "feat(scope): description"
125
138
  git push -u origin HEAD
126
139
  gh pr create --title "feat(scope): description" --body "..."
127
140
 
128
- # Update branch with latest main
129
- git fetch origin main && git rebase origin/main
141
+ # Update branch with the latest default branch
142
+ git fetch origin "$BASE" && git rebase "origin/$BASE"
130
143
  ```
131
144
 
132
145
  ## Principles
@@ -1,7 +1,25 @@
1
1
  ---
2
- description: "Run code quality checks on a directory or file"
2
+ description: Run code quality checks on a directory or file
3
+ argument-hint: "[path]"
3
4
  allowed-tools:
4
- - Bash
5
+ - Bash(eslint:*)
6
+ - Bash(tsc:*)
7
+ - Bash(prettier:*)
8
+ - Bash(biome:*)
9
+ - Bash(ruff:*)
10
+ - Bash(mypy:*)
11
+ - Bash(pyright:*)
12
+ - Bash(black:*)
13
+ - Bash(cargo clippy:*)
14
+ - Bash(cargo check:*)
15
+ - Bash(rustfmt:*)
16
+ - Bash(gofmt:*)
17
+ - Bash(go vet:*)
18
+ - Bash(golangci-lint:*)
19
+ - Bash(npm run:*)
20
+ - Bash(pnpm:*)
21
+ - Bash(bun run:*)
22
+ - Bash(yarn:*)
5
23
  - Read
6
24
  - Glob
7
25
  - Grep
@@ -11,6 +29,8 @@ allowed-tools:
11
29
 
12
30
  Run lint, typecheck, and format checks against the target directory or file. Report all issues found and suggest fixes.
13
31
 
32
+ **Target:** `$ARGUMENTS` (defaults to the current directory if empty).
33
+
14
34
  ## Workflow
15
35
 
16
36
  1. **Detect project tooling** by reading `package.json`, `pyproject.toml`, `Makefile`, `Cargo.toml`, or equivalent config files in the project root. Identify which linter, typechecker, and formatter are configured.
@@ -1,7 +1,12 @@
1
1
  ---
2
- description: "Onboard yourself to a codebase area or task"
2
+ description: Onboard yourself to a codebase area or task
3
+ argument-hint: "[area]"
3
4
  allowed-tools:
4
- - Bash
5
+ - Bash(git log:*)
6
+ - Bash(git ls-files:*)
7
+ - Bash(git remote:*)
8
+ - Bash(ls:*)
9
+ - Bash(find:*)
5
10
  - Read
6
11
  - Glob
7
12
  - Grep
@@ -13,7 +18,7 @@ Systematically explore and understand a codebase area, module, or task context.
13
18
 
14
19
  ## Workflow
15
20
 
16
- 1. **Understand the scope.** The user will specify an area (e.g., "the auth module", "the payments flow", "this repo"). If no scope is given, start from the project root.
21
+ 1. **Understand the scope.** The scope is given by `$ARGUMENTS` (e.g., "the auth module", "the payments flow", "this repo"). If `$ARGUMENTS` is empty, start from the project root.
17
22
 
18
23
  2. **Read foundational files first:**
19
24
  - `README.md`, `CLAUDE.md`, `CONTRIBUTING.md` (project conventions)
@@ -1,104 +1,38 @@
1
1
  ---
2
- description: "Review a pull request using project standards"
2
+ description: Review a pull request using project standards
3
+ argument-hint: "[pr-number]"
3
4
  allowed-tools:
4
- - Bash
5
+ - Bash(gh pr view:*)
6
+ - Bash(gh pr diff:*)
7
+ - Bash(gh pr checkout:*)
8
+ - Bash(git diff:*)
9
+ - Bash(git log:*)
10
+ - Bash(git merge-base:*)
11
+ - Bash(git branch:*)
5
12
  - Read
6
- - Glob
7
- - Grep
13
+ - Task
8
14
  ---
9
15
 
10
16
  # Pull Request Review
11
17
 
12
- Perform a checklist-based code review on the current PR or a specified PR number. Evaluate against project standards and common quality criteria.
18
+ Review the current branch's PR, or a specific PR number passed as `$ARGUMENTS`, by delegating the analysis to the `ct-code-reviewer` agent — the single source of truth for the review checklist, severity model, and output format.
13
19
 
14
20
  ## Workflow
15
21
 
16
- 1. **Get PR context.** If a PR number is provided, fetch it with `gh pr view {number} --json title,body,files,commits`. Otherwise, diff the current branch against the base branch (`main` or `master`).
22
+ 1. **Get PR context.**
23
+ - If `$ARGUMENTS` contains a PR number, fetch the PR's **actual** changes (metadata alone has no patch):
24
+ - `gh pr view $ARGUMENTS --json title,body,files,commits` for the title, description, and file list.
25
+ - `gh pr diff $ARGUMENTS` to read the actual patch.
26
+ - If you need to read changed files at the PR's revision rather than the local checkout, run `gh pr checkout $ARGUMENTS` first so the working tree matches the PR branch.
27
+ - Otherwise (no PR number), diff the current branch against its base branch (`main` or `master`): resolve the base with `git merge-base`, then use `git diff {base}..HEAD` and `git log {base}..HEAD --oneline --no-merges`.
17
28
 
18
- 2. **Read all changed files** in full to understand the complete context of the change. Do not review diffs in isolation — understand what the surrounding code does.
29
+ 2. **Delegate the review to the `ct-code-reviewer` agent.** Spawn the `ct-code-reviewer` agent with the Task tool, handing it the PR title/description and the diff (and, when you ran `gh pr checkout`, permission to read the changed files in full). The agent applies its own review checklist, severity model, and output format — do not restate them here.
19
30
 
20
- 3. **Run the review checklist** against every changed file.
21
-
22
- 4. **Compile findings** into a structured review.
23
-
24
- ## Review Checklist
25
-
26
- ### Logic Correctness
27
- - Are conditionals and branches correct? Any off-by-one errors?
28
- - Are edge cases handled (empty inputs, null values, boundary conditions)?
29
- - Does the control flow match the intended behavior?
30
- - Are there any unreachable code paths?
31
-
32
- ### Type Safety
33
- - Are types precise (no `any`, no unsafe casts, no overly broad unions)?
34
- - Is type narrowing used correctly before accessing properties?
35
- - Are generic constraints appropriate?
36
- - Are return types explicit where they should be?
37
-
38
- ### Error Handling
39
- - Are errors caught and handled meaningfully (not swallowed)?
40
- - Do error messages provide enough context for debugging?
41
- - Are error boundaries or fallbacks in place where needed?
42
- - Is cleanup logic (finally blocks, defer, etc.) correct?
43
-
44
- ### Test Coverage
45
- - Are new behaviors covered by tests?
46
- - Do tests focus on behavior, not implementation details?
47
- - Are edge cases and error paths tested?
48
- - Are test descriptions clear and descriptive?
49
-
50
- ### Performance
51
- - Any unnecessary re-renders, recomputations, or allocations?
52
- - Any N+1 query patterns or missing pagination?
53
- - Any missing memoization for expensive operations?
54
- - Any potential memory leaks (unclosed resources, dangling listeners)?
55
-
56
- ### Security
57
- - Any injection vulnerabilities (SQL, XSS, command injection)?
58
- - Are user inputs validated and sanitized?
59
- - Are auth checks present on protected operations?
60
- - Are secrets or sensitive data properly handled (not logged, not exposed)?
61
-
62
- ### Code Style
63
- - Does naming follow project conventions?
64
- - Is the code readable without excessive comments?
65
- - Are abstractions at the right level (not over- or under-engineered)?
66
- - Is there unnecessary duplication?
67
-
68
- ## Output Format
69
-
70
- ```markdown
71
- ## PR Review: {title}
72
-
73
- **Branch:** {branch} -> {base}
74
- **Files changed:** {count}
75
- **Verdict:** {APPROVE | REQUEST_CHANGES | COMMENT}
76
-
77
- ### Critical Issues
78
- > Must be fixed before merge.
79
-
80
- - **[CRITICAL]** {file}:{line} - {description}
81
-
82
- ### Warnings
83
- > Should be addressed, but not blocking.
84
-
85
- - **[WARNING]** {file}:{line} - {description}
86
-
87
- ### Suggestions
88
- > Nice-to-have improvements.
89
-
90
- - **[SUGGESTION]** {file}:{line} - {description}
91
-
92
- ### Positive Notes
93
- - {things done well}
94
-
95
- ### Summary
96
- {1-3 sentence overall assessment}
97
- ```
31
+ 3. **Return the agent's review to the user verbatim.** Add a one-line note only if you had to skip something (e.g. the PR was too large to check out, or `gh` was unavailable).
98
32
 
99
33
  ## Notes
100
34
 
101
- - Be specific. Reference file names and line numbers.
102
- - Distinguish between opinion and objective issues.
103
- - Acknowledge good patterns, not just problems.
35
+ - The `ct-code-reviewer` agent is the canonical review checklist and output format. This command only gathers context and delegates, so the checklist and severity model never drift between the two.
36
+ - Prefer `gh pr diff` / `gh pr checkout` over reading local files when a PR number is given — the local branch is usually not the PR branch.
104
37
  - If the PR is too large to review effectively, say so and suggest splitting it.
38
+ - If `gh` is not installed or the PR number is invalid, fall back to reviewing the current branch's diff against its base.
@@ -1,7 +1,10 @@
1
1
  ---
2
- description: "Generate a PR summary from the current branch"
2
+ description: Generate a PR summary from the current branch
3
3
  allowed-tools:
4
- - Bash
4
+ - Bash(git diff:*)
5
+ - Bash(git log:*)
6
+ - Bash(git merge-base:*)
7
+ - Bash(git branch:*)
5
8
  - Read
6
9
  ---
7
10
 
@@ -15,7 +18,7 @@ Generate a structured pull request summary by analyzing the diff and commit hist
15
18
 
16
19
  2. **Gather the diff.** Run `git diff {base}...HEAD --stat` for an overview and `git diff {base}...HEAD` for full changes.
17
20
 
18
- 3. **Gather commit history.** Run `git log {base}...HEAD --oneline --no-merges` to understand the progression of changes.
21
+ 3. **Gather commit history.** Run `git log {base}..HEAD --oneline --no-merges` (two-dot: commits on HEAD but not on base) to understand the progression of changes.
19
22
 
20
23
  4. **Analyze changes** by reading modified files to understand the "why" behind each change. Group related changes together.
21
24
 
@@ -1,7 +1,24 @@
1
1
  ---
2
- description: "Work end-to-end on a ticket or issue"
2
+ description: Work end-to-end on a ticket or issue
3
+ argument-hint: "[issue-number | description]"
3
4
  allowed-tools:
4
- - Bash
5
+ - Bash(git status:*)
6
+ - Bash(git checkout:*)
7
+ - Bash(git switch:*)
8
+ - Bash(git branch:*)
9
+ - Bash(git add:*)
10
+ - Bash(git commit:*)
11
+ - Bash(git push:*)
12
+ - Bash(git diff:*)
13
+ - Bash(git log:*)
14
+ - Bash(gh issue view:*)
15
+ - Bash(gh pr create:*)
16
+ - Bash(gh pr view:*)
17
+ - Bash(npm run:*)
18
+ - Bash(pnpm:*)
19
+ - Bash(bun run:*)
20
+ - Bash(bun test:*)
21
+ - Bash(yarn:*)
5
22
  - Read
6
23
  - Write
7
24
  - Edit
@@ -17,7 +34,7 @@ Work on a ticket or issue end-to-end: understand it, explore the codebase, plan
17
34
 
18
35
  ### Phase 1: Understand
19
36
 
20
- 1. **Read the ticket.** If a GitHub issue number is provided, fetch it with `gh issue view {number}`. Otherwise, use the description provided by the user.
37
+ 1. **Read the ticket.** If `$ARGUMENTS` is a GitHub issue number, fetch it with `gh issue view $ARGUMENTS`. Otherwise, treat `$ARGUMENTS` as the ticket description.
21
38
 
22
39
  2. **Clarify requirements.** Identify acceptance criteria, edge cases, and constraints. If anything is ambiguous, ask before proceeding.
23
40
 
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  /**
3
2
  * Skill Evaluation Engine v2.0
4
3
  *
@@ -18,6 +17,10 @@ const path = require("node:path");
18
17
  // Configuration
19
18
  const RULES_PATH = path.join(__dirname, "skill-rules.json");
20
19
 
20
+ // Cap the text handed to the (potentially quadratic) regex scans. Long single-line
21
+ // pastes — minified bundles, one-line JSON — can otherwise blow past the 5s hook timeout.
22
+ const MAX_PROMPT_LENGTH = 50_000;
23
+
21
24
  /**
22
25
  * @typedef {Object} SkillMatch
23
26
  * @property {string} name - Skill name
@@ -36,7 +39,7 @@ function loadRules() {
36
39
  return JSON.parse(content);
37
40
  } catch (error) {
38
41
  console.error(`Failed to load skill rules: ${error.message}`);
39
- process.exit(0);
42
+ return null;
40
43
  }
41
44
  }
42
45
 
@@ -46,29 +49,33 @@ function loadRules() {
46
49
  * @returns {string[]} Array of detected file paths
47
50
  */
48
51
  function extractFilePaths(prompt) {
52
+ // Normalize backslashes so Windows paths match, and cap length for the regex scans.
53
+ const text = String(prompt).slice(0, MAX_PROMPT_LENGTH).replace(/\\/g, "/");
49
54
  const paths = new Set();
50
55
 
51
- // Match explicit paths with extensions
56
+ // Match explicit paths with extensions. Negative lookbehind lets a candidate start
57
+ // after any non-path delimiter — "(", ",", ":", "=", whitespace, quotes, or BOL.
52
58
  const extensionPattern =
53
- /(?:^|\s|["'`])([\w\-./]+\.(?:[tj]sx?|json|gql|ya?ml|md|sh|rs|proto|toml|css\.ts|wasm))\b/gi;
54
- for (const match of prompt.matchAll(extensionPattern)) {
59
+ /(?<![\w\-./])([\w\-./]+\.(?:[tj]sx?|json|gql|ya?ml|md|sh|rs|proto|toml|wasm))\b/gi;
60
+ for (const match of text.matchAll(extensionPattern)) {
55
61
  paths.add(match[1]);
56
62
  }
57
63
 
58
- // Match paths starting with common directories
64
+ // Match paths starting with common directories.
59
65
  const dirPattern =
60
- /(?:^|\s|["'`])((?:src|app|components|worker|workers|proto|hooks|utils|services|styles|locales|tests|\.claude|\.github)\/[\w\-./]+)/gi;
61
- for (const match of prompt.matchAll(dirPattern)) {
66
+ /(?<![\w\-./])((?:src|app|components|worker|workers|proto|hooks|utils|services|styles|locales|tests|\.claude|\.github)\/[\w\-./]+)/gi;
67
+ for (const match of text.matchAll(dirPattern)) {
62
68
  paths.add(match[1]);
63
69
  }
64
70
 
65
- // Match quoted paths
71
+ // Match quoted paths.
66
72
  const quotedPattern = /["'`]([\w\-./]+\/[\w\-./]+)["'`]/g;
67
- for (const match of prompt.matchAll(quotedPattern)) {
73
+ for (const match of text.matchAll(quotedPattern)) {
68
74
  paths.add(match[1]);
69
75
  }
70
76
 
71
- return Array.from(paths);
77
+ // Require at least one letter so ratios/fractions like "3/4" are not treated as paths.
78
+ return Array.from(paths).filter((p) => /[A-Za-z]/.test(p));
72
79
  }
73
80
 
74
81
  /**
@@ -94,18 +101,19 @@ function matchesPattern(text, pattern, flags = "i") {
94
101
  * @returns {boolean}
95
102
  */
96
103
  function matchesGlob(filePath, globPattern) {
104
+ const normalized = String(filePath).replace(/\\/g, "/");
97
105
  const regexPattern = globPattern
98
- .replace(/\./g, "\\.")
99
- .replace(/\*\*\//g, "<<<DOUBLESTARSLASH>>>")
100
- .replace(/\*\*/g, "<<<DOUBLESTAR>>>")
106
+ .replace(/[.+^${}()|[\]]/g, "\\$&")
107
+ .replace(/\*\*\//g, "<<<GLOBSTARSLASH>>>")
108
+ .replace(/\*\*/g, "<<<GLOBSTAR>>>")
101
109
  .replace(/\*/g, "[^/]*")
102
- .replace(/<<<DOUBLESTARSLASH>>>/g, "(.*\\/)?")
103
- .replace(/<<<DOUBLESTAR>>>/g, ".*")
104
- .replace(/\?/g, ".");
110
+ .replace(/\?/g, ".")
111
+ .replace(/<<<GLOBSTARSLASH>>>/g, "(?:.*/)?")
112
+ .replace(/<<<GLOBSTAR>>>/g, ".*");
105
113
 
106
114
  try {
107
115
  const regex = new RegExp(`^${regexPattern}$`, "i");
108
- return regex.test(filePath);
116
+ return regex.test(normalized);
109
117
  } catch {
110
118
  return false;
111
119
  }
@@ -257,20 +265,48 @@ function formatConfidence(score, minScore) {
257
265
  return "LOW";
258
266
  }
259
267
 
268
+ // Strip characters that could break out of the <user-prompt-submit-hook> block or
269
+ // forge system-reminder content when a rule-derived string is echoed into output (F04).
270
+ function sanitize(value) {
271
+ return String(value)
272
+ .replace(/[<>\r\n]/g, " ")
273
+ .trim();
274
+ }
275
+
276
+ // Confirm the skill actually ships before ordering Claude to invoke it (F03).
277
+ // baseDir is .claude/skills at runtime (path.join(__dirname, "..", "skills")).
278
+ function skillExists(name, skillsDir) {
279
+ try {
280
+ return fs.existsSync(path.join(skillsDir, name, "SKILL.md"));
281
+ } catch {
282
+ return false;
283
+ }
284
+ }
285
+
260
286
  /**
261
287
  * Main evaluation function
262
288
  */
263
- function evaluate(prompt) {
264
- const rules = loadRules();
265
- const { config, skills } = rules;
266
-
267
- const promptLower = prompt.toLowerCase();
268
- const filePaths = extractFilePaths(prompt);
289
+ function evaluate(prompt, opts = {}) {
290
+ const rules = opts.rules !== undefined ? opts.rules : loadRules();
291
+ if (!rules?.skills) return "";
292
+ const skillsDir = opts.skillsDir ?? path.join(__dirname, "..", "skills");
293
+
294
+ const {
295
+ minConfidenceScore = 3,
296
+ maxSkillsToShow = 5,
297
+ showMatchReasons = true,
298
+ } = rules.config ?? {};
299
+
300
+ // Cap the text before the pattern scans (F09), and coerce non-strings (F21).
301
+ const cappedPrompt = String(prompt).slice(0, MAX_PROMPT_LENGTH);
302
+ const promptLower = cappedPrompt.toLowerCase();
303
+ const filePaths = extractFilePaths(cappedPrompt);
304
+ const skills = rules.skills;
269
305
 
270
306
  const matches = [];
271
307
  for (const [name, skill] of Object.entries(skills)) {
272
- const match = evaluateSkill(name, skill, prompt, promptLower, filePaths, rules);
273
- if (match && match.score >= config.minConfidenceScore) {
308
+ const match = evaluateSkill(name, skill, cappedPrompt, promptLower, filePaths, rules);
309
+ if (match && match.score >= minConfidenceScore && skillExists(match.name, skillsDir)) {
274
310
  matches.push(match);
275
311
  }
276
312
  }
@@ -284,29 +320,31 @@ function evaluate(prompt) {
284
320
  return b.priority - a.priority;
285
321
  });
286
322
 
287
- const topMatches = matches.slice(0, config.maxSkillsToShow);
288
- const relatedSkills = getRelatedSkills(topMatches, skills);
323
+ const topMatches = matches.slice(0, maxSkillsToShow);
324
+ const relatedSkills = getRelatedSkills(topMatches, skills).filter((n) =>
325
+ skillExists(n, skillsDir),
326
+ );
289
327
 
290
328
  let output = "<user-prompt-submit-hook>\n";
291
329
  output += "SKILL ACTIVATION REQUIRED\n\n";
292
330
 
293
331
  if (filePaths.length > 0) {
294
- output += `Detected file paths: ${filePaths.join(", ")}\n\n`;
332
+ output += `Detected file paths: ${filePaths.map(sanitize).join(", ")}\n\n`;
295
333
  }
296
334
 
297
335
  output += "Matched skills (ranked by relevance):\n";
298
336
 
299
337
  for (let i = 0; i < topMatches.length; i++) {
300
338
  const match = topMatches[i];
301
- const confidence = formatConfidence(match.score, config.minConfidenceScore);
302
- output += `${i + 1}. ${match.name} (${confidence} confidence)\n`;
303
- if (config.showMatchReasons && match.reasons.length > 0) {
304
- output += ` Matched: ${match.reasons.slice(0, 3).join(", ")}\n`;
339
+ const confidence = formatConfidence(match.score, minConfidenceScore);
340
+ output += `${i + 1}. ${sanitize(match.name)} (${confidence} confidence)\n`;
341
+ if (showMatchReasons && match.reasons.length > 0) {
342
+ output += ` Matched: ${match.reasons.slice(0, 3).map(sanitize).join(", ")}\n`;
305
343
  }
306
344
  }
307
345
 
308
346
  if (relatedSkills.length > 0) {
309
- output += `\nRelated skills to consider: ${relatedSkills.join(", ")}\n`;
347
+ output += `\nRelated skills to consider: ${relatedSkills.map(sanitize).join(", ")}\n`;
310
348
  }
311
349
 
312
350
  output += "\nBefore implementing, you MUST:\n";
@@ -314,9 +352,9 @@ function evaluate(prompt) {
314
352
  output += "2. ACTIVATE: Invoke the Skill tool for each YES skill\n";
315
353
  output += "3. IMPLEMENT: Only proceed after skill activation\n";
316
354
  output += "\nExample evaluation:\n";
317
- output += `- ${topMatches[0].name}: YES - [your reasoning]\n`;
355
+ output += `- ${sanitize(topMatches[0].name)}: YES - [your reasoning]\n`;
318
356
  if (topMatches.length > 1) {
319
- output += `- ${topMatches[1].name}: NO - [your reasoning]\n`;
357
+ output += `- ${sanitize(topMatches[1].name)}: NO - [your reasoning]\n`;
320
358
  }
321
359
  output += "\nDO NOT skip this step. Invoke relevant skills NOW.\n";
322
360
  output += "</user-prompt-submit-hook>";
@@ -334,18 +372,25 @@ function main() {
334
372
  input += chunk;
335
373
  });
336
374
 
375
+ // F36: never leave stdin errors unhandled (would otherwise hang to the hook timeout).
376
+ process.stdin.on("error", () => {
377
+ process.exitCode = 0;
378
+ });
379
+
337
380
  process.stdin.on("end", () => {
338
381
  let prompt = "";
339
382
 
340
383
  try {
341
384
  const data = JSON.parse(input);
342
- prompt = data.prompt || "";
385
+ // F21: only accept a string prompt; anything else is treated as empty.
386
+ prompt = typeof data.prompt === "string" ? data.prompt : "";
343
387
  } catch {
344
388
  prompt = input;
345
389
  }
346
390
 
347
- if (!prompt.trim()) {
348
- process.exit(0);
391
+ if (typeof prompt !== "string" || !prompt.trim()) {
392
+ process.exitCode = 0;
393
+ return;
349
394
  }
350
395
 
351
396
  try {
@@ -357,8 +402,25 @@ function main() {
357
402
  console.error(`Skill evaluation failed: ${error.message}`);
358
403
  }
359
404
 
360
- process.exit(0);
405
+ // F35: set exitCode and let the process drain stdout naturally rather than
406
+ // process.exit(0), which can truncate a large console.log on POSIX pipes.
407
+ process.exitCode = 0;
361
408
  });
362
409
  }
363
410
 
364
- main();
411
+ if (require.main === module) {
412
+ main();
413
+ }
414
+
415
+ module.exports = {
416
+ matchesGlob,
417
+ matchesPattern,
418
+ extractFilePaths,
419
+ matchDirectoryMapping,
420
+ evaluateSkill,
421
+ getRelatedSkills,
422
+ formatConfidence,
423
+ skillExists,
424
+ sanitize,
425
+ evaluate,
426
+ };