claude-dev-env 1.2.1 → 1.7.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.
@@ -175,7 +175,7 @@ Displays updated context
175
175
 
176
176
  ### With Hooks
177
177
 
178
- **SessionStart Hook** (~/.claude/hooks/hooks.json):
178
+ **SessionStart Hook** (~/.claude/settings.json):
179
179
  ```json
180
180
  {
181
181
  "name": "auto-load-project-context",
package/bin/install.mjs CHANGED
@@ -5,6 +5,7 @@ import { join, dirname, resolve, relative } from 'node:path';
5
5
  import { homedir } from 'node:os';
6
6
  import { execSync } from 'node:child_process';
7
7
  import { fileURLToPath } from 'node:url';
8
+ import { createRequire } from 'node:module';
8
9
 
9
10
  const CLAUDE_HOME = join(homedir(), '.claude');
10
11
  const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
@@ -12,6 +13,7 @@ const MANIFEST_FILE = join(CLAUDE_HOME, '.claude-dev-env-manifest.json');
12
13
  const PACKAGE_NAME = 'claude-dev-env';
13
14
 
14
15
  const CONTENT_DIRECTORIES = ['rules', 'docs', 'commands', 'agents'];
16
+ const WORKSPACE_SIBLINGS = ['claude-journal', 'claude-deep-research', 'claude-prompt-tools'];
15
17
 
16
18
  function detectPython() {
17
19
  const candidates = [
@@ -114,6 +116,67 @@ function mergeHooks(pythonCommand) {
114
116
  return groupCount;
115
117
  }
116
118
 
119
+ function findSiblingPackage(packageName) {
120
+ const workspaceRoot = resolve(PACKAGE_ROOT, '..', '..');
121
+ const workspacePath = join(workspaceRoot, 'packages', packageName);
122
+ if (existsSync(join(workspacePath, 'package.json'))) {
123
+ return workspacePath;
124
+ }
125
+ const require = createRequire(import.meta.url);
126
+ try {
127
+ const packageJsonPath = require.resolve(join(packageName, 'package.json'));
128
+ return dirname(packageJsonPath);
129
+ } catch { /* not found */ }
130
+ return null;
131
+ }
132
+
133
+ function installSiblingContent(siblingRoot, allInstalledFiles) {
134
+ const siblingName = JSON.parse(readFileSync(join(siblingRoot, 'package.json'), 'utf8')).name;
135
+ console.log(` Installing sibling: ${siblingName}`);
136
+ let totalFiles = 0;
137
+ const skillsSource = join(siblingRoot, 'skills');
138
+ if (existsSync(skillsSource)) {
139
+ const skillDirs = readdirSync(skillsSource, { withFileTypes: true }).filter(entry => entry.isDirectory());
140
+ for (const skillDir of skillDirs) {
141
+ const stats = copyTree(join(skillsSource, skillDir.name), join(CLAUDE_HOME, 'skills', skillDir.name));
142
+ allInstalledFiles.push(...stats.paths);
143
+ totalFiles += stats.created + stats.updated;
144
+ }
145
+ }
146
+ const agentsSource = join(siblingRoot, 'agents');
147
+ if (existsSync(agentsSource)) {
148
+ const stats = copyTree(agentsSource, join(CLAUDE_HOME, 'agents'));
149
+ allInstalledFiles.push(...stats.paths);
150
+ totalFiles += stats.created + stats.updated;
151
+ }
152
+ const rulesSource = join(siblingRoot, 'rules');
153
+ if (existsSync(rulesSource)) {
154
+ const stats = copyTree(rulesSource, join(CLAUDE_HOME, 'rules'));
155
+ allInstalledFiles.push(...stats.paths);
156
+ totalFiles += stats.created + stats.updated;
157
+ }
158
+ const hooksSource = join(siblingRoot, 'hooks');
159
+ if (existsSync(hooksSource)) {
160
+ const filesToCopy = collectFiles(hooksSource).filter(file => !file.endsWith('hooks.json'));
161
+ const hooksDestination = join(CLAUDE_HOME, 'hooks');
162
+ for (const sourceFile of filesToCopy) {
163
+ const relativePath = relative(hooksSource, sourceFile);
164
+ const destFile = join(hooksDestination, relativePath);
165
+ mkdirSync(dirname(destFile), { recursive: true });
166
+ const existed = existsSync(destFile);
167
+ copyFileSync(sourceFile, destFile);
168
+ allInstalledFiles.push(destFile);
169
+ totalFiles++;
170
+ if (existed) {
171
+ console.log(` ↻ ${join('hooks', relativePath)} (updated)`);
172
+ } else {
173
+ console.log(` ✓ ${join('hooks', relativePath)} (new)`);
174
+ }
175
+ }
176
+ }
177
+ return totalFiles;
178
+ }
179
+
117
180
  function writeManifest(installedFiles) {
118
181
  const manifest = { package: PACKAGE_NAME, version: '1.0.0', installedAt: new Date().toISOString(), files: installedFiles };
119
182
  writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2) + '\n');
@@ -153,6 +216,16 @@ function install() {
153
216
  summary.skills = { created: skillsCreated, updated: skillsUpdated, paths: skillPaths };
154
217
  allInstalledFiles.push(...skillPaths);
155
218
  }
219
+ let siblingCount = 0;
220
+ for (const siblingName of WORKSPACE_SIBLINGS) {
221
+ const siblingRoot = findSiblingPackage(siblingName);
222
+ if (siblingRoot) {
223
+ siblingCount += installSiblingContent(siblingRoot, allInstalledFiles);
224
+ }
225
+ }
226
+ if (siblingCount > 0) {
227
+ summary.siblings = siblingCount;
228
+ }
156
229
  const hooksSource = join(PACKAGE_ROOT, 'hooks');
157
230
  if (existsSync(hooksSource)) {
158
231
  const hooksDestination = join(CLAUDE_HOME, 'hooks');
@@ -186,6 +259,9 @@ function install() {
186
259
  const { created, updated } = summary.skills;
187
260
  console.log(` skills: ${created + updated} files (${created} new, ${updated} updated)`);
188
261
  }
262
+ if (summary.siblings) {
263
+ console.log(` workspace siblings: ${summary.siblings} files`);
264
+ }
189
265
  if (summary.hookFiles) {
190
266
  console.log(` hooks: ${summary.hookFiles.created + summary.hookFiles.updated} files, ${summary.hookGroups} groups in settings.json`);
191
267
  }
@@ -0,0 +1,95 @@
1
+ # GitHub PR Summary Writing Guide for AI
2
+
3
+ Use this guide when writing pull request descriptions. Follow these best practices to create clear, professional, and reviewable PR summaries.
4
+
5
+ ## Required Sections
6
+
7
+ ### What (Changes)
8
+
9
+ - Concise statement of what was changed
10
+ - What files or systems were modified
11
+ - What functionality was added, removed, or improved
12
+ - Keep to 2-3 sentences maximum
13
+
14
+ ### Why (Problem/Context)
15
+
16
+ - Explain the problem this PR solves
17
+ - Provide business or technical context
18
+ - Reference related issue numbers using `#123` or `Fixes #123`, `Closes #456`
19
+ - If no issue exists, briefly explain the motivation
20
+
21
+ ### How (Approach/Solution)
22
+
23
+ - Describe your implementation approach
24
+ - Explain any design decisions or trade-offs
25
+ - Include architectural changes if applicable
26
+ - Note any breaking changes prominently
27
+
28
+ ## Supporting Details
29
+
30
+ ### Testing and Quality
31
+
32
+ - What tests were added/modified
33
+ - How to manually verify the changes (if applicable)
34
+ - Any areas of concern or limitations
35
+ - Performance impact (if relevant)
36
+
37
+ ### Dependencies and Risk
38
+
39
+ - New dependencies introduced (if any)
40
+ - Backward compatibility status
41
+ - Potential side effects
42
+ - Migration steps (if needed)
43
+
44
+ ## Optional but Valuable
45
+
46
+ ### Related Issues/PRs
47
+
48
+ - Link to dependent PRs or issues
49
+ - Note any follow-up work needed
50
+
51
+ ### Screenshots/Examples (for UI changes)
52
+
53
+ - Before/after comparisons when visual changes are involved
54
+
55
+ ### Reviewer Guidance
56
+
57
+ - Specific areas to focus on
58
+ - Questions for reviewers
59
+ - Deployment considerations
60
+
61
+ ## Tone and Style Guidelines
62
+
63
+ - Be clear and concise -- reviewers scan quickly
64
+ - Use second person sparingly -- focus on what the code does, not what the reviewer should do
65
+ - Avoid jargon -- explain technical terms if non-obvious
66
+ - Use markdown formatting -- bullets, code blocks, headers for readability
67
+ - Be honest about limitations -- acknowledge trade-offs and known issues
68
+ - Assume reviewers are unfamiliar -- provide sufficient context
69
+
70
+ ## What to Avoid
71
+
72
+ - Vague statements like "fix bug" or "update code"
73
+ - AI-generated summaries without human verification
74
+ - Large walls of text -- break into sections
75
+ - Repeating information from commit messages
76
+ - References to temporary branch names or internal jargon without context
77
+
78
+ ## Example Structure
79
+
80
+ ```markdown
81
+ ## Description
82
+ Brief 1-2 sentence overview of the change.
83
+
84
+ ## Why
85
+ Problem/context and reference to related issue (#123).
86
+
87
+ ## How
88
+ Implementation approach and design decisions.
89
+
90
+ ## Testing
91
+ How this was tested and verified.
92
+
93
+ ## Risk Assessment
94
+ Any breaking changes, dependencies, or concerns.
95
+ ```
@@ -123,9 +123,21 @@ def parse_bash_command_from_stdin() -> str:
123
123
  return hook_event.get("tool_input", {}).get("command", "")
124
124
 
125
125
 
126
+ DRAFT_PR_INSTRUCTION = (
127
+ " Instead: (1) create a feature branch with `git checkout -b <descriptive-branch-name>`, "
128
+ "(2) commit your changes there, "
129
+ "(3) push with `git push -u origin <branch-name>`, "
130
+ "(4) create a draft PR with `gh pr create --draft`. "
131
+ "If you must commit to main, the user needs to approve explicitly."
132
+ )
133
+
134
+
126
135
  def build_denial_response(branch_name: str, repo_dir: str | None) -> dict:
127
136
  location = f" in {repo_dir}" if repo_dir else ""
128
- denial_reason = f"BLOCKED: Direct commit to '{branch_name}' is not allowed. Create a feature branch first: git checkout -b feature/your-branch-name. If you must commit to main, the user needs to approve explicitly."
137
+ denial_reason = (
138
+ f"BLOCKED: Direct commit to '{branch_name}'{location} is not allowed."
139
+ + DRAFT_PR_INSTRUCTION
140
+ )
129
141
 
130
142
  return {
131
143
  "hookSpecificOutput": {
@@ -1,9 +1,67 @@
1
1
  import json
2
+ import os
2
3
  import re
3
4
  import sys
4
5
 
6
+ PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
7
+ PR_GUIDE_PATH = os.path.join(PLUGIN_ROOT, "docs", "PR_DESCRIPTION_GUIDE.md")
5
8
 
6
- def main():
9
+ REQUIRED_PR_SECTION_HEADERS = [
10
+ "description",
11
+ "why",
12
+ "how",
13
+ ]
14
+
15
+ MINIMUM_PR_BODY_LENGTH = 50
16
+
17
+ VAGUE_LANGUAGE_PATTERN = re.compile(
18
+ r'\b(fix(?:ed)? (?:bug|issue|it)|update(?:d)? code|minor changes|various (?:fixes|updates|improvements))\b',
19
+ re.IGNORECASE,
20
+ )
21
+
22
+
23
+ def extract_body_from_command(command: str) -> str:
24
+ heredoc_match = re.search(r'--body\s+"\$\(cat <<', command)
25
+ if heredoc_match:
26
+ return command[heredoc_match.start():]
27
+
28
+ body_match = re.search(r'--body\s+"([^"]*)"', command) or re.search(r"--body\s+'([^']*)'", command)
29
+ if body_match:
30
+ return body_match.group(1)
31
+
32
+ short_flag_match = re.search(r'-b\s+"([^"]*)"', command) or re.search(r"-b\s+'([^']*)'", command)
33
+ if short_flag_match:
34
+ return short_flag_match.group(1)
35
+
36
+ return ""
37
+
38
+
39
+ def validate_pr_body(body: str) -> list[str]:
40
+ violations = []
41
+ body_lower = body.lower()
42
+
43
+ missing_required_sections = [
44
+ header for header in REQUIRED_PR_SECTION_HEADERS
45
+ if f"## {header}" not in body_lower and f"**{header}" not in body_lower
46
+ ]
47
+
48
+ if missing_required_sections:
49
+ formatted_sections = ", ".join(f"'{each_section.title()}'" for each_section in missing_required_sections)
50
+ violations.append(f"Missing required section(s): {formatted_sections}")
51
+
52
+ stripped_body = re.sub(r'#.*', '', body).strip()
53
+ stripped_body = re.sub(r'\*\*.*?\*\*', '', stripped_body).strip()
54
+ if len(stripped_body) < MINIMUM_PR_BODY_LENGTH:
55
+ violations.append("PR body too short -- provide meaningful context for reviewers")
56
+
57
+ vague_matches = VAGUE_LANGUAGE_PATTERN.findall(body)
58
+ if vague_matches:
59
+ violations.append(f"Vague language detected: {', '.join(vague_matches)} -- be specific about what changed and why")
60
+
61
+ return violations
62
+
63
+
64
+ def main() -> None:
7
65
  try:
8
66
  input_data = json.load(sys.stdin)
9
67
  except json.JSONDecodeError:
@@ -19,62 +77,29 @@ def main():
19
77
 
20
78
  is_pr_create = "gh pr create" in command and ("--body" in command or "-b " in command)
21
79
  is_pr_edit = "gh pr edit" in command and "--body" in command
22
- is_commit = re.search(r'git commit\b', command) and ("-m " in command or "-m\"" in command or "-m'" in command)
23
80
 
24
- if not (is_pr_create or is_pr_edit or is_commit):
81
+ if not (is_pr_create or is_pr_edit):
25
82
  sys.exit(0)
26
83
 
27
- body = ""
28
- if is_pr_create or is_pr_edit:
29
- body_match = re.search(r'--body\s+"([^"]*)"', command) or re.search(r"--body\s+'([^']*)'", command)
30
- if body_match:
31
- body = body_match.group(1)
32
- heredoc_match = re.search(r'--body\s+"\$\(cat <<', command)
33
- if heredoc_match:
34
- body = command[heredoc_match.start():]
35
-
36
- if is_commit:
37
- msg_match = re.search(r'-m\s+"([^"]*)"', command) or re.search(r"-m\s+'([^']*)'", command)
38
- if msg_match:
39
- body = msg_match.group(1)
40
- heredoc_match = re.search(r'-m\s+"\$\(cat <<', command)
41
- if heredoc_match:
42
- body = command[heredoc_match.start():]
84
+ body = extract_body_from_command(command)
43
85
 
44
86
  if not body:
45
87
  sys.exit(0)
46
88
 
47
- violations = []
48
-
49
- if is_pr_create or is_pr_edit:
50
- if "## Summary" not in body and "## summary" not in body.lower():
51
- violations.append("Missing '## Summary' section")
52
-
53
- has_file_bold = bool(re.search(r'\*\*\w+\.\w+\*\*', body))
54
- has_bullet_section = bool(re.search(r'###.*(?:test|config|fix)', body, re.IGNORECASE))
55
-
56
- if not has_file_bold and not has_bullet_section:
57
- violations.append("Production changes must be grouped by file with **filename** bold headers explaining WHY")
58
-
59
- jargon_patterns = [
60
- (r'\bDexie\b', "Dexie (say 'database' or 'local database')"),
61
- (r'\bReact Query\b', "React Query (say 'cache' or 'data cache')"),
62
- (r'\bsyncStatus\b', "syncStatus (describe the behavior, not the field)"),
63
- (r'\blocalUpdatedAt\b', "localUpdatedAt (describe the behavior, not the field)"),
64
- (r'\bpullStartedAt\b', "pullStartedAt (describe the behavior, not the field)"),
65
- (r'\buseMutation\b', "useMutation (describe what it does for the user)"),
66
- ]
67
- for pattern, name in jargon_patterns:
68
- if re.search(pattern, body):
69
- violations.append(f"Jargon detected: {name}")
89
+ violations = validate_pr_body(body)
70
90
 
71
91
  if violations:
72
92
  violation_list = "; ".join(violations)
93
+ pr_guide_reference = f" @{PR_GUIDE_PATH}" if os.path.exists(PR_GUIDE_PATH) else ""
94
+ denial_reason = (
95
+ f"BLOCKED: [PR_DESCRIPTION] {violation_list}. "
96
+ f"Follow the PR description guide:{pr_guide_reference}"
97
+ )
73
98
  result = {
74
99
  "hookSpecificOutput": {
75
100
  "hookEventName": "PreToolUse",
76
101
  "permissionDecision": "deny",
77
- "permissionDecisionReason": f"BLOCKED: [PR_DESCRIPTION_STYLE] {violation_list}. Use the pr-description-writer custom agent: Agent(subagent_type=\"pr-description-writer\", team_name=\"your-team\", prompt=\"Write PR description for the current branch\").",
102
+ "permissionDecisionReason": denial_reason,
78
103
  }
79
104
  }
80
105
  print(json.dumps(result))
package/hooks/hooks.json CHANGED
@@ -44,11 +44,6 @@
44
44
  "type": "command",
45
45
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/validation/code-style-validator.py",
46
46
  "timeout": 15
47
- },
48
- {
49
- "type": "command",
50
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/docker-settings-guard.py",
51
- "timeout": 15
52
47
  }
53
48
  ]
54
49
  },
@@ -117,11 +112,6 @@
117
112
  {
118
113
  "matcher": "",
119
114
  "hooks": [
120
- {
121
- "type": "command",
122
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/hook-structure-context.py",
123
- "timeout": 10
124
- },
125
115
  {
126
116
  "type": "command",
127
117
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/bulk-edit-reminder.py",
@@ -136,16 +126,6 @@
136
126
  }
137
127
  ],
138
128
  "SessionStart": [
139
- {
140
- "matcher": "compact",
141
- "hooks": [
142
- {
143
- "type": "command",
144
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/compact-context-reinject.py",
145
- "timeout": 10
146
- }
147
- ]
148
- },
149
129
  {
150
130
  "matcher": "",
151
131
  "hooks": [
@@ -11,12 +11,27 @@ This catches:
11
11
  Works in both WSL and Windows for any Python project with a git root.
12
12
  Project root is discovered via CLAUDE_PROJECT_ROOT env var or git rev-parse.
13
13
  """
14
+ import importlib
14
15
  import json
15
16
  import os
16
17
  import platform
17
18
  import subprocess
18
19
  import sys
19
20
  from pathlib import Path
21
+ from types import ModuleType
22
+
23
+ NOTIFICATION_UTILS_DIRECTORY = os.path.join(
24
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "notification"
25
+ )
26
+ sys.path.insert(0, NOTIFICATION_UTILS_DIRECTORY)
27
+
28
+
29
+ def load_notification_utils() -> ModuleType | None:
30
+ try:
31
+ return importlib.import_module("notification_utils")
32
+ except ImportError:
33
+ return None
34
+
20
35
 
21
36
  IS_WINDOWS = platform.system() == "Windows"
22
37
 
@@ -104,6 +119,25 @@ def format_error_summary(all_error_lines: list[str]) -> str:
104
119
  return error_summary
105
120
 
106
121
 
122
+ def send_block_notification(error_summary: str) -> None:
123
+ notification_module = load_notification_utils()
124
+ if notification_module is None:
125
+ return
126
+
127
+ notification_title = "Mypy Type Errors"
128
+ notification_body = f"Write blocked: {error_summary[:200]}"
129
+
130
+ try:
131
+ if notification_module.is_wsl():
132
+ notification_module.notify_wsl(notification_title, notification_body)
133
+ elif platform.system() == "Linux":
134
+ notification_module.notify_linux()
135
+ elif platform.system() == "Windows":
136
+ notification_module.notify_windows(notification_title, notification_body)
137
+ except (AttributeError, OSError):
138
+ pass
139
+
140
+
107
141
  def build_block_response(error_summary: str) -> dict[str, str | dict[str, str]]:
108
142
  return {
109
143
  "decision": "block",
@@ -171,6 +205,7 @@ def main() -> None:
171
205
  sys.exit(0)
172
206
 
173
207
  error_summary = format_error_summary(all_error_lines)
208
+ send_block_notification(error_summary)
174
209
  block_response = build_block_response(error_summary)
175
210
  print(json.dumps(block_response))
176
211
  sys.exit(0)
@@ -1,10 +1,45 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
+ import importlib
3
4
  import json
4
5
  import os
6
+ import platform
5
7
  import subprocess
6
8
  import sys
7
9
  from pathlib import Path
10
+ from types import ModuleType
11
+
12
+ NOTIFICATION_UTILS_DIRECTORY = os.path.join(
13
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "notification"
14
+ )
15
+ sys.path.insert(0, NOTIFICATION_UTILS_DIRECTORY)
16
+
17
+
18
+ def load_notification_utils() -> ModuleType | None:
19
+ try:
20
+ return importlib.import_module("notification_utils")
21
+ except ImportError:
22
+ return None
23
+
24
+
25
+ def send_format_notification(file_path: str, formatter_name: str) -> None:
26
+ notification_module = load_notification_utils()
27
+ if notification_module is None:
28
+ return
29
+
30
+ notification_title = "Auto-Formatter"
31
+ notification_body = f"{formatter_name} formatted: {Path(file_path).name}"
32
+
33
+ try:
34
+ if notification_module.is_wsl():
35
+ notification_module.notify_wsl(notification_title, notification_body)
36
+ elif platform.system() == "Linux":
37
+ notification_module.notify_linux()
38
+ elif platform.system() == "Windows":
39
+ notification_module.notify_windows(notification_title, notification_body)
40
+ except (AttributeError, OSError):
41
+ pass
42
+
8
43
 
9
44
  PYTHON_EXTENSIONS = {".py"}
10
45
  JS_EXTENSIONS = {".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs"}
@@ -89,6 +124,8 @@ def main() -> None:
89
124
  try:
90
125
  format_run = subprocess.run(each_formatter_command, capture_output=True, text=True, timeout=PYTHON_FORMAT_TIMEOUT_SECONDS)
91
126
  if format_run.returncode == 0:
127
+ formatter_name = each_formatter_command[0] if each_formatter_command[0] != sys.executable else each_formatter_command[2]
128
+ send_format_notification(file_path, formatter_name)
92
129
  break
93
130
  except FileNotFoundError:
94
131
  continue
@@ -98,12 +135,14 @@ def main() -> None:
98
135
  if not has_prettier_config(file_path):
99
136
  sys.exit(0)
100
137
  try:
101
- subprocess.run(
138
+ prettier_run = subprocess.run(
102
139
  ["npx", "--yes", "prettier", "--write", file_path],
103
140
  capture_output=True,
104
141
  text=True,
105
142
  timeout=JS_FORMAT_TIMEOUT_SECONDS,
106
143
  )
144
+ if prettier_run.returncode == 0:
145
+ send_format_notification(file_path, "prettier")
107
146
  except (FileNotFoundError, subprocess.TimeoutExpired):
108
147
  pass
109
148
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.2.1",
3
+ "version": "1.7.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,6 +15,11 @@
15
15
  "skills/",
16
16
  "hooks/"
17
17
  ],
18
+ "dependencies": {
19
+ "claude-journal": "^1.3.0",
20
+ "claude-deep-research": "^1.0.0",
21
+ "claude-prompt-tools": "^1.0.0"
22
+ },
18
23
  "keywords": [
19
24
  "claude-code",
20
25
  "plugin",
@@ -25,6 +30,7 @@
25
30
  "license": "MIT",
26
31
  "repository": {
27
32
  "type": "git",
28
- "url": "git+https://github.com/jl-cmd/claude-code-config.git"
33
+ "url": "git+https://github.com/jl-cmd/claude-code-config.git",
34
+ "directory": "packages/claude-dev-env"
29
35
  }
30
36
  }
@@ -29,7 +29,7 @@ Scan the current repo root for these directories:
29
29
  | `commands/` | Slash commands (.md) | `~/.claude/commands/` |
30
30
  | `agents/` | Agent definitions (.md) | `~/.claude/agents/` |
31
31
  | `skills/` | Skill packages (subdirs) | `~/.claude/skills/` |
32
- | `hooks/` | Hook scripts + hooks.json | Merge into `~/.claude/settings.json` |
32
+ | `hooks/` | Hook scripts (+ optional hooks.json manifest) | Merge into `~/.claude/settings.json` |
33
33
 
34
34
  Use Glob to find which directories exist and count files in each. Report findings to the user.
35
35
 
@@ -97,6 +97,10 @@ If hooks/hooks.json exists:
97
97
  - Append new groups
98
98
  - Write settings.json with `JSON.stringify(data, null, 4)`
99
99
 
100
+ Important runtime note:
101
+ - `~/.claude/settings.json` is the runtime source of truth for hooks.
102
+ - `hooks/hooks.json` is a packaging/install manifest input for merge workflows, not a runtime file Claude reads directly.
103
+
100
104
  Print summary:
101
105
  ```
102
106
  Installed <package-name>:
@@ -176,8 +180,8 @@ When ready to publish: `npm publish`
176
180
  ## Remember
177
181
 
178
182
  - Zero external dependencies — only Node.js built-ins
179
- - hooks.json in the repo stays canonical (python3 + ${CLAUDE_PLUGIN_ROOT})
180
- - Rewriting happens only in the destination settings.json
183
+ - If present, hooks.json in the repo stays canonical (python3 + ${CLAUDE_PLUGIN_ROOT})
184
+ - Rewriting happens only in the destination settings.json (runtime source of truth)
181
185
  - path.join() everywhere — never concatenate paths with `/` or `\`
182
186
  - Idempotent: running twice produces the same result
183
187
  - Log every file action so the user sees what changed