@softspark/ai-toolkit 1.0.0 → 1.2.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 (59) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/README.md +22 -12
  3. package/app/.claude-plugin/plugin.json +2 -2
  4. package/app/ARCHITECTURE.md +7 -5
  5. package/app/hooks/pre-compact.sh +44 -10
  6. package/app/personas/backend-lead.md +27 -0
  7. package/app/personas/devops-eng.md +27 -0
  8. package/app/personas/frontend-lead.md +26 -0
  9. package/app/personas/junior-dev.md +28 -0
  10. package/app/rules/claude-toolkit-rules.md +1 -1
  11. package/app/skills/api-patterns/SKILL.md +1 -0
  12. package/app/skills/app-builder/SKILL.md +1 -0
  13. package/app/skills/ci-cd-patterns/SKILL.md +1 -0
  14. package/app/skills/clean-code/SKILL.md +1 -0
  15. package/app/skills/csharp-patterns/SKILL.md +1 -0
  16. package/app/skills/database-patterns/SKILL.md +1 -0
  17. package/app/skills/debugging-tactics/SKILL.md +1 -1
  18. package/app/skills/design-engineering/SKILL.md +1 -0
  19. package/app/skills/docker-devops/SKILL.md +1 -0
  20. package/app/skills/documentation-standards/SKILL.md +1 -0
  21. package/app/skills/ecommerce-patterns/SKILL.md +1 -0
  22. package/app/skills/flutter-patterns/SKILL.md +1 -0
  23. package/app/skills/git-mastery/SKILL.md +1 -1
  24. package/app/skills/hive-mind/SKILL.md +1 -0
  25. package/app/skills/java-patterns/SKILL.md +1 -0
  26. package/app/skills/kotlin-patterns/SKILL.md +1 -0
  27. package/app/skills/mcp-patterns/SKILL.md +1 -0
  28. package/app/skills/migration-patterns/SKILL.md +1 -0
  29. package/app/skills/observability-patterns/SKILL.md +1 -0
  30. package/app/skills/onboard/SKILL.md +2 -0
  31. package/app/skills/orchestrate/SKILL.md +1 -1
  32. package/app/skills/performance-profiling/SKILL.md +1 -1
  33. package/app/skills/persona/SKILL.md +57 -0
  34. package/app/skills/plan-writing/SKILL.md +1 -0
  35. package/app/skills/pr/scripts/pr-summary.py +6 -3
  36. package/app/skills/rag-patterns/SKILL.md +1 -0
  37. package/app/skills/research-mastery/SKILL.md +1 -0
  38. package/app/skills/ruby-patterns/SKILL.md +1 -0
  39. package/app/skills/rust-patterns/SKILL.md +1 -0
  40. package/app/skills/security-patterns/SKILL.md +1 -0
  41. package/app/skills/skill-audit/SKILL.md +137 -0
  42. package/app/skills/subagent-development/SKILL.md +1 -1
  43. package/app/skills/swarm/SKILL.md +1 -1
  44. package/app/skills/swift-patterns/SKILL.md +1 -1
  45. package/app/skills/teams/SKILL.md +1 -1
  46. package/app/skills/testing-patterns/SKILL.md +1 -0
  47. package/app/skills/typescript-patterns/SKILL.md +1 -0
  48. package/app/skills/verification-before-completion/SKILL.md +1 -0
  49. package/app/skills/workflow/SKILL.md +1 -1
  50. package/kb/reference/architecture-overview.md +28 -5
  51. package/kb/reference/hooks-catalog.md +6 -4
  52. package/kb/reference/skills-catalog.md +5 -3
  53. package/llms.txt +2 -2
  54. package/package.json +3 -2
  55. package/scripts/audit_skills.py +290 -0
  56. package/scripts/generate_augment.py +46 -0
  57. package/scripts/install.py +32 -2
  58. package/scripts/install_steps/ai_tools.py +10 -1
  59. package/scripts/install_steps/hooks.py +30 -0
@@ -0,0 +1,290 @@
1
+ #!/usr/bin/env python3
2
+ """Skill & Agent Security Auditor.
3
+
4
+ Deterministic scanner for ai-toolkit skills and agents.
5
+ Detects dangerous code patterns, hardcoded secrets, and permission issues.
6
+
7
+ Stdlib-only. JSON output to stdout. Non-zero exit on HIGH findings.
8
+
9
+ Usage:
10
+ python3 scripts/audit_skills.py [toolkit-dir] # scan all
11
+ python3 scripts/audit_skills.py [toolkit-dir] --json # JSON output
12
+ python3 scripts/audit_skills.py [toolkit-dir] --ci # exit 1 on HIGH
13
+
14
+ Exit codes:
15
+ 0 no HIGH findings
16
+ 1 HIGH findings detected (or errors)
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import re
22
+ import sys
23
+ from pathlib import Path
24
+
25
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
26
+ from _common import toolkit_dir as default_toolkit_dir
27
+ from frontmatter import frontmatter_field
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Pattern definitions
31
+ # ---------------------------------------------------------------------------
32
+
33
+ PYTHON_HIGH = [
34
+ (r'\beval\s*\(', "eval() — arbitrary code execution"),
35
+ (r'\bexec\s*\(', "exec() — arbitrary code execution"),
36
+ (r'\bos\.system\s*\(', "os.system() — shell injection risk"),
37
+ (r'subprocess\.[a-z]+\(.*shell\s*=\s*True', "subprocess with shell=True — shell injection"),
38
+ (r'\b__import__\s*\(', "__import__() — dynamic import"),
39
+ (r'\bpickle\.loads?\s*\(', "pickle.load/loads — deserialization attack"),
40
+ (r'\bmarshall?\.loads?\s*\(', "marshal.load/loads — deserialization attack"),
41
+ ]
42
+
43
+ PYTHON_WARN = [
44
+ (r'open\s*\(.*["\']w["\']', "open() with write mode — verify path is validated"),
45
+ (r'\bcompile\s*\(.*\bexec\b', "compile() with exec — dynamic code generation"),
46
+ (r'\bgetattr\s*\(.*input', "getattr with user input — attribute injection risk"),
47
+ ]
48
+
49
+ BASH_HIGH = [
50
+ (r'curl\s+.*\|\s*(ba)?sh', "curl | bash — remote code execution"),
51
+ (r'wget\s+.*\|\s*(ba)?sh', "wget | bash — remote code execution"),
52
+ (r'rm\s+-rf\s+/', "rm -rf / — destructive"),
53
+ (r'rm\s+-rf\s+~', "rm -rf ~ — destructive"),
54
+ (r'rm\s+-rf\s+\$HOME', "rm -rf $HOME — destructive"),
55
+ ]
56
+
57
+ BASH_WARN = [
58
+ (r'chmod\s+(-R\s+)?777', "chmod 777 — overly permissive"),
59
+ (r'eval\s+"\$', "eval with variable — injection risk"),
60
+ ]
61
+
62
+ SECRET_PATTERNS = [
63
+ (r'AKIA[0-9A-Z]{16}', "HIGH", "AWS access key"),
64
+ (r'sk-[a-zA-Z0-9]{20,}', "HIGH", "API key (sk-* pattern)"),
65
+ (r'ghp_[a-zA-Z0-9]{36}', "HIGH", "GitHub personal access token"),
66
+ (r'gho_[a-zA-Z0-9]{36}', "HIGH", "GitHub OAuth token"),
67
+ (r'-----BEGIN\s+(RSA|DSA|EC|OPENSSH)?\s*PRIVATE\s+KEY', "HIGH", "Private key"),
68
+ (r'password\s*=\s*["\'][^"\']{4,}["\']', "WARN", "Hardcoded password"),
69
+ (r'token\s*=\s*["\'][^"\']{8,}["\']', "WARN", "Hardcoded token"),
70
+ (r'secret\s*=\s*["\'][^"\']{8,}["\']', "WARN", "Hardcoded secret"),
71
+ (r'api_key\s*=\s*["\'][^"\']{8,}["\']', "WARN", "Hardcoded API key"),
72
+ ]
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Scanner
76
+ # ---------------------------------------------------------------------------
77
+
78
+ class Finding:
79
+ __slots__ = ("severity", "file", "line", "pattern", "description")
80
+
81
+ def __init__(self, severity: str, file: str, line: int,
82
+ pattern: str, description: str) -> None:
83
+ self.severity = severity
84
+ self.file = file
85
+ self.line = line
86
+ self.pattern = pattern
87
+ self.description = description
88
+
89
+ def to_dict(self) -> dict:
90
+ return {
91
+ "severity": self.severity,
92
+ "file": self.file,
93
+ "line": self.line,
94
+ "pattern": self.pattern,
95
+ "description": self.description,
96
+ }
97
+
98
+
99
+ def scan_file_patterns(filepath: Path, patterns: list[tuple],
100
+ severity: str, findings: list[Finding]) -> None:
101
+ """Scan a single file against a list of regex patterns."""
102
+ try:
103
+ text = filepath.read_text(encoding="utf-8", errors="replace")
104
+ except OSError:
105
+ return
106
+ rel = str(filepath)
107
+ for lineno, line in enumerate(text.splitlines(), 1):
108
+ for regex, desc in patterns:
109
+ if re.search(regex, line, re.IGNORECASE):
110
+ findings.append(Finding(severity, rel, lineno, regex, desc))
111
+
112
+
113
+ def scan_secrets(filepath: Path, findings: list[Finding]) -> None:
114
+ """Scan a file for hardcoded secrets."""
115
+ try:
116
+ text = filepath.read_text(encoding="utf-8", errors="replace")
117
+ except OSError:
118
+ return
119
+ rel = str(filepath)
120
+ for lineno, line in enumerate(text.splitlines(), 1):
121
+ for regex, severity, desc in SECRET_PATTERNS:
122
+ if re.search(regex, line):
123
+ findings.append(Finding(severity, rel, lineno, regex, desc))
124
+
125
+
126
+ def check_frontmatter(skill_dir: Path, findings: list[Finding]) -> None:
127
+ """Check SKILL.md frontmatter for permission issues."""
128
+ skill_md = skill_dir / "SKILL.md"
129
+ if not skill_md.is_file():
130
+ return
131
+ rel = str(skill_md)
132
+
133
+ allowed = frontmatter_field(skill_md, "allowed-tools")
134
+ user_invocable = frontmatter_field(skill_md, "user-invocable")
135
+ disable_model = frontmatter_field(skill_md, "disable-model-invocation")
136
+
137
+ # Knowledge skill with Bash = suspicious
138
+ if user_invocable == "false" and "Bash" in allowed:
139
+ findings.append(Finding(
140
+ "HIGH", rel, 0,
141
+ "knowledge-skill-with-bash",
142
+ "Knowledge skill (user-invocable: false) has Bash access — "
143
+ "auto-loaded skills should not execute shell commands",
144
+ ))
145
+
146
+ # No allowed-tools at all = unrestricted
147
+ if not allowed and not disable_model:
148
+ findings.append(Finding(
149
+ "WARN", rel, 0,
150
+ "missing-allowed-tools",
151
+ "No allowed-tools declared — skill has unrestricted tool access. "
152
+ "Add allowed-tools with principle of least privilege.",
153
+ ))
154
+
155
+ # Overly permissive: Bash + Write + Edit without justification
156
+ if allowed:
157
+ tools = {t.strip() for t in allowed.split(",")}
158
+ if {"Bash", "Write", "Edit"}.issubset(tools) and len(tools) >= 5:
159
+ findings.append(Finding(
160
+ "INFO", rel, 0,
161
+ "broad-tool-access",
162
+ f"Skill has broad tool access ({len(tools)} tools including Bash+Write+Edit). "
163
+ "Verify this is necessary.",
164
+ ))
165
+
166
+
167
+ def check_agent(agent_md: Path, findings: list[Finding]) -> None:
168
+ """Check agent definition for issues."""
169
+ rel = str(agent_md)
170
+ tools = frontmatter_field(agent_md, "tools")
171
+ if tools == "*":
172
+ findings.append(Finding(
173
+ "INFO", rel, 0,
174
+ "agent-all-tools",
175
+ "Agent has access to all tools (*). Consider restricting.",
176
+ ))
177
+
178
+
179
+ # ---------------------------------------------------------------------------
180
+ # Main
181
+ # ---------------------------------------------------------------------------
182
+
183
+ def audit(toolkit_root: Path) -> list[Finding]:
184
+ """Run full audit and return findings."""
185
+ findings: list[Finding] = []
186
+ app = toolkit_root / "app"
187
+ skills = app / "skills"
188
+ agents = app / "agents"
189
+
190
+ # Scan skills
191
+ if skills.is_dir():
192
+ for skill_dir in sorted(skills.iterdir()):
193
+ if not skill_dir.is_dir():
194
+ continue
195
+ # Frontmatter checks
196
+ check_frontmatter(skill_dir, findings)
197
+
198
+ # Python scripts
199
+ for py in skill_dir.rglob("*.py"):
200
+ scan_file_patterns(py, PYTHON_HIGH, "HIGH", findings)
201
+ scan_file_patterns(py, PYTHON_WARN, "WARN", findings)
202
+ scan_secrets(py, findings)
203
+
204
+ # Bash scripts
205
+ for sh in skill_dir.rglob("*.sh"):
206
+ scan_file_patterns(sh, BASH_HIGH, "HIGH", findings)
207
+ scan_file_patterns(sh, BASH_WARN, "WARN", findings)
208
+ scan_secrets(sh, findings)
209
+
210
+ # Secrets in any text file
211
+ for md in skill_dir.rglob("*.md"):
212
+ scan_secrets(md, findings)
213
+
214
+ # Scan agents
215
+ if agents.is_dir():
216
+ for agent_md in sorted(agents.glob("*.md")):
217
+ check_agent(agent_md, findings)
218
+ scan_secrets(agent_md, findings)
219
+
220
+ # Sort: HIGH first, then WARN, then INFO
221
+ order = {"HIGH": 0, "WARN": 1, "INFO": 2}
222
+ findings.sort(key=lambda f: (order.get(f.severity, 9), f.file, f.line))
223
+ return findings
224
+
225
+
226
+ def print_text(findings: list[Finding]) -> None:
227
+ """Print human-readable report."""
228
+ high = sum(1 for f in findings if f.severity == "HIGH")
229
+ warn = sum(1 for f in findings if f.severity == "WARN")
230
+ info = sum(1 for f in findings if f.severity == "INFO")
231
+
232
+ print("Skill Security Audit")
233
+ print("=" * 40)
234
+ print(f"HIGH: {high} | WARN: {warn} | INFO: {info}")
235
+ print()
236
+
237
+ if not findings:
238
+ print("No findings. All clear.")
239
+ return
240
+
241
+ for f in findings:
242
+ loc = f"{f.file}:{f.line}" if f.line else f.file
243
+ print(f"[{f.severity}] {loc}")
244
+ print(f" {f.description}")
245
+ print()
246
+
247
+
248
+ def print_json(findings: list[Finding]) -> None:
249
+ """Print JSON report."""
250
+ high = sum(1 for f in findings if f.severity == "HIGH")
251
+ report = {
252
+ "summary": {
253
+ "high": high,
254
+ "warn": sum(1 for f in findings if f.severity == "WARN"),
255
+ "info": sum(1 for f in findings if f.severity == "INFO"),
256
+ "total": len(findings),
257
+ },
258
+ "findings": [f.to_dict() for f in findings],
259
+ }
260
+ print(json.dumps(report, indent=2))
261
+
262
+
263
+ def main() -> None:
264
+ args = sys.argv[1:]
265
+ toolkit_root = default_toolkit_dir
266
+ json_mode = False
267
+ ci_mode = False
268
+
269
+ for arg in args:
270
+ if arg == "--json":
271
+ json_mode = True
272
+ elif arg == "--ci":
273
+ ci_mode = True
274
+ elif not arg.startswith("-"):
275
+ toolkit_root = Path(arg)
276
+
277
+ findings = audit(toolkit_root)
278
+
279
+ if json_mode:
280
+ print_json(findings)
281
+ else:
282
+ print_text(findings)
283
+
284
+ high_count = sum(1 for f in findings if f.severity == "HIGH")
285
+ if ci_mode and high_count > 0:
286
+ sys.exit(1)
287
+
288
+
289
+ if __name__ == "__main__":
290
+ main()
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env python3
2
+ """Generate Augment AI rules from app/agents/*.md and app/skills/*/SKILL.md.
3
+
4
+ Augment uses ~/.augment/rules/*.md (user-level, always_apply) or
5
+ <project>/.augment/rules/*.md (workspace-level) with optional frontmatter.
6
+
7
+ Global install target: ~/.augment/rules/ai-toolkit.md
8
+ Local install target: .augment/rules/ai-toolkit.md
9
+
10
+ Usage: python3 scripts/generate_augment.py > .augment/rules/ai-toolkit.md
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
18
+ from generator_base import render_generator
19
+
20
+ if __name__ == "__main__":
21
+ render_generator({
22
+ "title": "# AI Toolkit — Augment Rules",
23
+ "header_lines": [
24
+ "---",
25
+ "type: always_apply",
26
+ "description: AI development toolkit with specialized agents and skills",
27
+ "---",
28
+ "",
29
+ "# Auto-generated by ai-toolkit — do not edit manually.",
30
+ "# Regenerate: ai-toolkit update",
31
+ ],
32
+ "intro_template": (
33
+ "This repository uses the ai-toolkit — a shared AI development toolkit"
34
+ " with {agents} specialized agent personas and {skills} skills."
35
+ ),
36
+ "agents_section": "## Available Agent Personas",
37
+ "agents_intro": "Apply the expertise of these agents when working on relevant tasks:",
38
+ "agents_format": "bullets",
39
+ "agents_level": "##",
40
+ "skills_section": "## Available Skills",
41
+ "skills_intro": "The following skills are available:",
42
+ "skills_format": "bullets",
43
+ "skills_level": "##",
44
+ "trailing_newline_after_skills": True,
45
+ "guidelines": ["general"],
46
+ })
@@ -29,9 +29,10 @@ Options:
29
29
  --list, --dry-run Dry-run: show what would be installed
30
30
  --reset Wipe and recreate local configs
31
31
  --profile <p> minimal|standard|strict
32
+ --persona <p> backend-lead|frontend-lead|devops-eng|junior-dev
32
33
 
33
34
  Components: agents, skills, hooks, constitution, architecture, rules,
34
- cursor, windsurf, gemini
35
+ cursor, windsurf, gemini, augment
35
36
  """
36
37
  from __future__ import annotations
37
38
 
@@ -39,7 +40,7 @@ import sys
39
40
  from pathlib import Path
40
41
 
41
42
  sys.path.insert(0, str(Path(__file__).resolve().parent))
42
- from _common import toolkit_dir
43
+ from _common import toolkit_dir, app_dir, inject_rule
43
44
  from emission import agent_count as count_agents, skill_count as count_skills
44
45
 
45
46
  # Step modules
@@ -63,6 +64,7 @@ def parse_args(argv: list[str]) -> dict:
63
64
  "local": False,
64
65
  "reset": False,
65
66
  "profile": "",
67
+ "persona": "",
66
68
  }
67
69
  i = 0
68
70
  while i < len(argv):
@@ -88,6 +90,11 @@ def parse_args(argv: list[str]) -> dict:
88
90
  elif arg == "--profile":
89
91
  i += 1
90
92
  cfg["profile"] = argv[i] if i < len(argv) else ""
93
+ elif arg.startswith("--persona="):
94
+ cfg["persona"] = arg.split("=", 1)[1]
95
+ elif arg == "--persona":
96
+ i += 1
97
+ cfg["persona"] = argv[i] if i < len(argv) else ""
91
98
  elif arg.startswith("-"):
92
99
  print(f"Unknown option: {arg}")
93
100
  sys.exit(1)
@@ -188,6 +195,27 @@ def install_claude_code(target_dir: Path, hooks_scripts_dir: Path,
188
195
  inject_rules(claude_dir, target_dir, rules_dir, only, skip, dry_run)
189
196
 
190
197
 
198
+ VALID_PERSONAS = ("backend-lead", "frontend-lead", "devops-eng", "junior-dev")
199
+
200
+
201
+ def install_persona(target_dir: Path, persona: str, dry_run: bool) -> None:
202
+ """Inject a persona rule into CLAUDE.md."""
203
+ if not persona:
204
+ return
205
+ if persona not in VALID_PERSONAS:
206
+ print(f"Unknown persona: {persona} (valid: {', '.join(VALID_PERSONAS)})")
207
+ sys.exit(1)
208
+ persona_file = app_dir / "personas" / f"{persona}.md"
209
+ if not persona_file.is_file():
210
+ print(f" Persona file not found: {persona_file}")
211
+ return
212
+ if dry_run:
213
+ print(f" Would inject persona: {persona}")
214
+ return
215
+ inject_rule(persona_file, target_dir)
216
+ print(f" Persona applied: {persona}")
217
+
218
+
191
219
  def install_strict_git_hooks(profile: str, local: bool, dry_run: bool) -> None:
192
220
  if profile == "strict" and not local and not dry_run:
193
221
  cwd = Path.cwd()
@@ -210,6 +238,7 @@ def main() -> None:
210
238
  local: bool = cfg["local"]
211
239
  reset: bool = cfg["reset"]
212
240
  profile: str = cfg["profile"]
241
+ persona: str = cfg["persona"]
213
242
 
214
243
  rules_dir = Path.home() / ".ai-toolkit" / "rules"
215
244
  hooks_scripts_dir = Path.home() / ".ai-toolkit" / "hooks"
@@ -228,6 +257,7 @@ def main() -> None:
228
257
  if local:
229
258
  install_local_project(rules_dir, dry_run, reset)
230
259
 
260
+ install_persona(target_dir, persona, dry_run)
231
261
  install_strict_git_hooks(profile, local, dry_run)
232
262
  print_summary()
233
263
 
@@ -1,4 +1,4 @@
1
- """Install AI tool configs (Cursor, Windsurf, Gemini) and local project setup."""
1
+ """Install AI tool configs (Cursor, Windsurf, Gemini, Augment) and local project setup."""
2
2
  from __future__ import annotations
3
3
 
4
4
  import shutil
@@ -47,6 +47,15 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
47
47
  else:
48
48
  print(" Skipped: gemini")
49
49
 
50
+ if should_install("augment", only, skip):
51
+ augment_file = target_dir / ".augment" / "rules" / "ai-toolkit.md"
52
+ if dry_run:
53
+ print(" Would inject: ~/.augment/rules/ai-toolkit.md")
54
+ else:
55
+ inject_with_rules("generate-augment.sh", augment_file, rules_dir)
56
+ else:
57
+ print(" Skipped: augment")
58
+
50
59
  print()
51
60
  print(" Note: Copilot, Cline, Roo Code, and Aider have no global config -- use 'ai-toolkit install --local' per project")
52
61
 
@@ -25,6 +25,7 @@ def install_hooks(claude_dir: Path, hooks_scripts_dir: Path,
25
25
  output_styles_dir = app_dir / "output-styles"
26
26
  if output_styles_dir.is_dir():
27
27
  print(" Would install: output styles to ~/.claude/output-styles/ + set outputStyle in settings.json")
28
+ print(" Would set: env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 in settings.json")
28
29
  return
29
30
 
30
31
  _copy_hook_scripts(claude_dir, hooks_scripts_dir)
@@ -41,6 +42,7 @@ def install_hooks(claude_dir: Path, hooks_scripts_dir: Path,
41
42
  print(" Removed: .claude/hooks.json (legacy)")
42
43
 
43
44
  _install_output_styles(claude_dir)
45
+ _install_env_vars(claude_dir)
44
46
 
45
47
 
46
48
  def _copy_hook_scripts(claude_dir: Path, hooks_scripts_dir: Path) -> None:
@@ -88,3 +90,31 @@ def _install_output_styles(claude_dir: Path) -> None:
88
90
  with open(settings_path, "w", encoding="utf-8") as f:
89
91
  json.dump(settings_data, f, indent=4)
90
92
  print(" Set: outputStyle = 'Golden Rules' in settings.json")
93
+
94
+
95
+ # Toolkit env vars injected into settings.json
96
+ TOOLKIT_ENV_VARS = {
97
+ "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1",
98
+ }
99
+
100
+
101
+ def _install_env_vars(claude_dir: Path) -> None:
102
+ """Ensure toolkit env vars are set in settings.json."""
103
+ settings_path = claude_dir / "settings.json"
104
+ if settings_path.is_file():
105
+ with open(settings_path, encoding="utf-8") as f:
106
+ settings_data = json.load(f)
107
+ else:
108
+ settings_data = {}
109
+
110
+ env = settings_data.setdefault("env", {})
111
+ changed = False
112
+ for key, value in TOOLKIT_ENV_VARS.items():
113
+ if env.get(key) != value:
114
+ env[key] = value
115
+ changed = True
116
+
117
+ if changed:
118
+ with open(settings_path, "w", encoding="utf-8") as f:
119
+ json.dump(settings_data, f, indent=4)
120
+ print(" Set: env vars in settings.json (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1)")