claude-code-templates 1.29.2 → 1.29.4

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.
package/README.md CHANGED
@@ -5,6 +5,7 @@
5
5
  [![Sponsored by Z.AI](https://img.shields.io/badge/Sponsored%20by-Z.AI-2563eb?style=flat&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMMiAyMkgyMkwxMiAyWiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+)](https://z.ai/subscribe?ic=8JVLJQFSKB&utm_source=github&utm_medium=badge&utm_campaign=readme)
6
6
  [![Claude for Open Source](https://img.shields.io/badge/Claude-Open%20Source%20Program-D97757?style=flat&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSIzIiBmaWxsPSJ3aGl0ZSIvPjwvc3ZnPg==)](https://claude.com/contact-sales/claude-for-oss)
7
7
  [![Neon Open Source Program](https://img.shields.io/badge/Neon-Open%20Source%20Program-00E599?style=flat)](https://get.neon.com/4eCjZDz)
8
+ [![GitHub Sponsors](https://img.shields.io/badge/GitHub%20Sponsors-sponsor-EA4AAA?style=flat&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/davila7)
8
9
  [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-support-yellow?style=flat&logo=buy-me-a-coffee)](https://buymeacoffee.com/daniavila)
9
10
  [![GitHub stars](https://img.shields.io/github/stars/davila7/claude-code-templates.svg?style=social&label=Star)](https://github.com/davila7/claude-code-templates)
10
11
 
@@ -39,7 +40,8 @@
39
40
 
40
41
  **[Browse All Templates](https://aitmpl.com)** - Interactive web interface to explore and install 100+ agents, commands, settings, hooks, and MCPs.
41
42
 
42
- <img width="1049" height="855" alt="Screenshot 2025-08-19 at 08 09 24" src="https://github.com/user-attachments/assets/e3617410-9b1c-4731-87b7-a3858800b737" />
43
+ <img width="1787" height="958" alt="image" src="https://github.com/user-attachments/assets/d84feaa4-f871-4843-bbee-42d8f51b2f21" />
44
+
43
45
 
44
46
  ## šŸš€ Quick Installation
45
47
 
@@ -153,10 +155,12 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
153
155
  - **šŸ› Issues**: [GitHub Issues](https://github.com/davila7/claude-code-templates/issues)
154
156
 
155
157
  ## Stargazers over time
156
- [![Stargazers over time](https://starchart.cc/davila7/claude-code-templates.svg?variant=adaptive)](https://starchart.cc/davila7/claude-code-templates)
158
+ [![Stargazers over time](docs/star-history.svg)](https://www.aitmpl.com/component/skill/git/star-history-chart)
157
159
 
158
160
  ---
159
161
 
160
162
  **⭐ Found this useful? Give us a star to support the project!**
161
163
 
162
- [![Buy Me A Coffee](https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20coffee&slug=daniavila&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff)](https://buymeacoffee.com/daniavila)
164
+ [![Sponsor on GitHub](https://img.shields.io/badge/Sponsor%20on%20GitHub-%E2%9D%A4-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/davila7)
165
+
166
+ [![Buy Me A Coffee](https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20coffee&slug=daniavila&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff)](https://buymeacoffee.com/daniavila)
@@ -4,6 +4,7 @@ const { program } = require('commander');
4
4
  const chalk = require('chalk');
5
5
  const { createClaudeConfig } = require('../src/index');
6
6
  const { showBanner } = require('../src/tui');
7
+ const { captureCliError } = require('../src/error-reporting');
7
8
 
8
9
  const pkg = require('../package.json');
9
10
 
@@ -37,6 +38,7 @@ program
37
38
  .option('--setting <setting>', 'install specific setting component (supports comma-separated values)')
38
39
  .option('--hook <hook>', 'install specific hook component (supports comma-separated values)')
39
40
  .option('--skill <skill>', 'install specific skill component (supports comma-separated values)')
41
+ .option('--loop <loop>', 'install specific loop component and its referenced components (supports comma-separated values)')
40
42
  .option('--workflow <workflow>', 'install workflow from hash (#hash) OR workflow YAML (base64 encoded) when used with --agent/--command/--mcp')
41
43
  .option('--prompt <prompt>', 'execute the provided prompt in Claude Code after installation or in sandbox')
42
44
  .option('--create-agent <agent>', 'create a global agent accessible from anywhere (e.g., customer-support)')
@@ -62,6 +64,7 @@ program
62
64
  await createClaudeConfig(options);
63
65
  } catch (error) {
64
66
  console.error(chalk.red('Error:'), error.message);
67
+ await captureCliError(error, { command: 'createClaudeConfig' });
65
68
  process.exit(1);
66
69
  }
67
70
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-templates",
3
- "version": "1.28.13",
3
+ "version": "1.29.4",
4
4
  "description": "CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -0,0 +1,158 @@
1
+ /**
2
+ * ErrorReporting - Opt-in crash reporting to Sentry
3
+ *
4
+ * Unlike tracking-service.js (anonymous usage analytics, on by default),
5
+ * this NEVER sends anything unless the user explicitly opts in via
6
+ * CCT_ERROR_REPORTING=true. It always respects the existing opt-out flags
7
+ * (CCT_NO_TRACKING, CCT_NO_ANALYTICS, CI) so opt-out still wins over opt-in.
8
+ *
9
+ * No SDK dependency — sends directly to the Sentry envelope API via fetch(),
10
+ * matching the zero-dependency style used elsewhere in this project
11
+ * (cloudflare-workers/*\/sentry.js, dashboard/src/lib/api/error-tracking.ts).
12
+ */
13
+
14
+ // Public DSN for the "aitmpl-cli" Sentry project. Sentry DSNs are not
15
+ // secrets (they only allow sending events, not reading them) — this is the
16
+ // same pattern most public telemetry SDKs use. Override with
17
+ // CCT_SENTRY_DSN for local testing against a different project.
18
+ const DEFAULT_SENTRY_DSN = 'https://1c1637a93ea6fb81e3edae3d039bdae0@o42738.ingest.us.sentry.io/4511679145312256';
19
+ const SENTRY_DSN = process.env.CCT_SENTRY_DSN || DEFAULT_SENTRY_DSN;
20
+
21
+ /**
22
+ * Whether error reporting is allowed to run at all.
23
+ * Opt-in required (CCT_ERROR_REPORTING=true), and opt-out always wins.
24
+ */
25
+ function shouldReportErrors() {
26
+ if (process.env.CCT_ERROR_REPORTING !== 'true') {
27
+ return false;
28
+ }
29
+
30
+ if (process.env.CCT_NO_TRACKING === 'true' ||
31
+ process.env.CCT_NO_ANALYTICS === 'true' ||
32
+ process.env.CI === 'true') {
33
+ return false;
34
+ }
35
+
36
+ if (!SENTRY_DSN) {
37
+ if (process.env.CCT_DEBUG === 'true') {
38
+ console.debug('šŸ“Š CCT_ERROR_REPORTING is set but no DSN is configured; skipping.');
39
+ }
40
+ return false;
41
+ }
42
+
43
+ return true;
44
+ }
45
+
46
+ function parseDsn(dsn) {
47
+ try {
48
+ const url = new URL(dsn);
49
+ return {
50
+ publicKey: url.username,
51
+ host: url.host,
52
+ projectId: url.pathname.replace(/^\//, ''),
53
+ };
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Report a CLI error to Sentry. Opt-in only — see shouldReportErrors().
61
+ * Never throws; safe to call from any catch block.
62
+ *
63
+ * @param {Error} error
64
+ * @param {object} [context] - e.g. { command: 'install', componentType: 'agent' }
65
+ */
66
+ async function captureCliError(error, context = {}) {
67
+ if (!shouldReportErrors()) {
68
+ return;
69
+ }
70
+
71
+ try {
72
+ const parsed = parseDsn(SENTRY_DSN);
73
+ if (!parsed) return;
74
+
75
+ const message = error instanceof Error ? error.message : String(error);
76
+ const stack = error instanceof Error ? error.stack : undefined;
77
+ const eventId = require('crypto').randomUUID().replace(/-/g, '');
78
+ const timestamp = new Date().toISOString();
79
+
80
+ const event = {
81
+ event_id: eventId,
82
+ timestamp,
83
+ platform: 'node',
84
+ logger: 'cli',
85
+ environment: 'production',
86
+ tags: {
87
+ cli_version: getCliVersion(),
88
+ node_version: process.version,
89
+ platform: process.platform,
90
+ ...context,
91
+ },
92
+ extra: context,
93
+ exception: {
94
+ values: [{
95
+ type: error instanceof Error ? (error.name || 'Error') : 'Error',
96
+ value: message,
97
+ stacktrace: stack
98
+ ? { frames: [{ filename: 'cli', function: 'anonymous', context_line: stack }] }
99
+ : undefined,
100
+ }],
101
+ },
102
+ };
103
+
104
+ const envelopeHeader = JSON.stringify({ event_id: eventId, sent_at: timestamp });
105
+ const itemHeader = JSON.stringify({ type: 'event' });
106
+ const body = `${envelopeHeader}\n${itemHeader}\n${JSON.stringify(event)}\n`;
107
+
108
+ const endpoint = `https://${parsed.host}/api/${parsed.projectId}/envelope/`;
109
+ const authHeader = `Sentry sentry_version=7, sentry_client=aitmpl-cli/1.0, sentry_key=${parsed.publicKey}`;
110
+
111
+ if (typeof fetch !== 'function') {
112
+ // Node < 18 without global fetch — skip rather than pull in a dependency.
113
+ if (process.env.CCT_DEBUG === 'true') {
114
+ console.debug('šŸ“Š Error reporting requires Node 18+ (global fetch); skipping.');
115
+ }
116
+ return;
117
+ }
118
+
119
+ const controller = new AbortController();
120
+ const timeoutId = setTimeout(() => controller.abort(), 3000);
121
+
122
+ try {
123
+ await fetch(endpoint, {
124
+ method: 'POST',
125
+ headers: {
126
+ 'Content-Type': 'application/x-sentry-envelope',
127
+ 'X-Sentry-Auth': authHeader,
128
+ },
129
+ body,
130
+ signal: controller.signal,
131
+ });
132
+ if (process.env.CCT_DEBUG === 'true') {
133
+ console.debug('šŸ“Š Error reported to Sentry:', eventId);
134
+ }
135
+ } finally {
136
+ clearTimeout(timeoutId);
137
+ }
138
+ } catch (reportingError) {
139
+ // Error reporting must never crash the CLI or mask the original error.
140
+ if (process.env.CCT_DEBUG === 'true') {
141
+ console.debug('šŸ“Š Error reporting failed (non-critical):', reportingError.message);
142
+ }
143
+ }
144
+ }
145
+
146
+ function getCliVersion() {
147
+ try {
148
+ const path = require('path');
149
+ const fs = require('fs');
150
+ const packagePath = path.join(__dirname, '..', 'package.json');
151
+ const packageData = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
152
+ return packageData.version || 'unknown';
153
+ } catch {
154
+ return 'unknown';
155
+ }
156
+ }
157
+
158
+ module.exports = { captureCliError, shouldReportErrors };
@@ -143,7 +143,7 @@ async function createClaudeConfig(options = {}) {
143
143
  }
144
144
 
145
145
  // Handle multiple components installation (new approach)
146
- if (options.agent || options.command || options.mcp || options.setting || options.hook || options.skill) {
146
+ if (options.agent || options.command || options.mcp || options.setting || options.hook || options.skill || options.loop) {
147
147
  // If --workflow is used with components, treat it as YAML
148
148
  if (options.workflow) {
149
149
  options.yaml = options.workflow;
@@ -1652,6 +1652,123 @@ async function installIndividualSkill(skillName, targetDir, options) {
1652
1652
  }
1653
1653
  }
1654
1654
 
1655
+ /**
1656
+ * Parse the `components:` frontmatter field of a loop into {type, path} refs.
1657
+ * The field is a flat bracketed list of `type:path` tokens, e.g.
1658
+ * components: [agent:documentation/docs-architect, skill:git/create-pr]
1659
+ */
1660
+ function parseLoopReferencedComponents(loopContent) {
1661
+ const refs = [];
1662
+ if (!loopContent || !loopContent.startsWith('---')) return refs;
1663
+ const fmEnd = loopContent.indexOf('---', 3);
1664
+ if (fmEnd === -1) return refs;
1665
+ const frontmatter = loopContent.slice(3, fmEnd);
1666
+ for (const line of frontmatter.split('\n')) {
1667
+ if (!line.startsWith('components:')) continue;
1668
+ const value = line.slice(line.indexOf(':') + 1).trim().replace(/^\[|\]$/g, '');
1669
+ for (const token of value.split(',')) {
1670
+ const trimmed = token.trim().replace(/^["']|["']$/g, '');
1671
+ if (!trimmed) continue;
1672
+ const sep = trimmed.indexOf(':');
1673
+ if (sep === -1) continue;
1674
+ const type = trimmed.slice(0, sep).trim();
1675
+ const compPath = trimmed.slice(sep + 1).trim().replace(/\.(md|json)$/, '');
1676
+ if (type && compPath) refs.push({ type, path: compPath });
1677
+ }
1678
+ break;
1679
+ }
1680
+ return refs;
1681
+ }
1682
+
1683
+ /**
1684
+ * Install a loop component (markdown) into .claude/loops/, then auto-install
1685
+ * every component it references (agents, skills, hooks, commands, settings, mcps).
1686
+ */
1687
+ async function installIndividualLoop(loopName, targetDir, options = {}) {
1688
+ console.log(chalk.blue(`šŸ” Installing loop: ${loopName}`));
1689
+ const startTime = Date.now();
1690
+
1691
+ try {
1692
+ const githubUrl = `https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/loops/${loopName}.md`;
1693
+ console.log(chalk.gray(`šŸ“„ Downloading from GitHub (main branch)...`));
1694
+
1695
+ const response = await fetch(githubUrl);
1696
+ if (!response.ok) {
1697
+ if (response.status === 404) {
1698
+ console.log(chalk.red(`āŒ Loop "${loopName}" not found`));
1699
+ trackingService.trackInstallationOutcome('loop', loopName, 'failure', { errorType: 'not_found', durationMs: Date.now() - startTime, batchId: options.batchId });
1700
+ return false;
1701
+ }
1702
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1703
+ }
1704
+
1705
+ const loopContent = await response.text();
1706
+
1707
+ // Write the loop file to .claude/loops/ (flat, like agents)
1708
+ const loopsDir = path.join(targetDir, '.claude', 'loops');
1709
+ await fs.ensureDir(loopsDir);
1710
+ const fileName = loopName.includes('/') ? loopName.split('/').pop() : loopName;
1711
+ const targetFile = path.join(loopsDir, `${fileName}.md`);
1712
+ await fs.writeFile(targetFile, loopContent, 'utf8');
1713
+
1714
+ if (!options.silent) {
1715
+ console.log(chalk.green(`āœ… Loop "${loopName}" installed successfully!`));
1716
+ console.log(chalk.cyan(`šŸ“ Installed to: ${path.relative(targetDir, targetFile)}`));
1717
+ }
1718
+
1719
+ trackingService.trackDownload('loop', loopName, {
1720
+ installation_type: 'individual_loop',
1721
+ target_directory: path.relative(process.cwd(), targetDir),
1722
+ source: 'github_main'
1723
+ });
1724
+
1725
+ // Auto-install referenced components
1726
+ const refs = parseLoopReferencedComponents(loopContent);
1727
+ if (refs.length > 0) {
1728
+ console.log(chalk.blue(`\n🧩 Installing ${refs.length} referenced component(s) for this loop...`));
1729
+ const refOptions = { ...options, silent: true };
1730
+ for (const ref of refs) {
1731
+ try {
1732
+ switch (ref.type) {
1733
+ case 'agent':
1734
+ await installIndividualAgent(ref.path, targetDir, refOptions);
1735
+ break;
1736
+ case 'command':
1737
+ await installIndividualCommand(ref.path, targetDir, refOptions);
1738
+ break;
1739
+ case 'skill':
1740
+ await installIndividualSkill(ref.path, targetDir, refOptions);
1741
+ break;
1742
+ case 'hook':
1743
+ await installIndividualHook(ref.path, targetDir, refOptions);
1744
+ break;
1745
+ case 'setting':
1746
+ await installIndividualSetting(ref.path, targetDir, refOptions);
1747
+ break;
1748
+ case 'mcp':
1749
+ await installIndividualMCP(ref.path, targetDir, refOptions);
1750
+ break;
1751
+ default:
1752
+ console.log(chalk.yellow(` āš ļø Unknown referenced component type: ${ref.type}:${ref.path}`));
1753
+ continue;
1754
+ }
1755
+ console.log(chalk.green(` āœ“ ${ref.type}: ${ref.path}`));
1756
+ } catch (refError) {
1757
+ console.log(chalk.yellow(` āš ļø Could not install ${ref.type}:${ref.path} (${refError.message})`));
1758
+ }
1759
+ }
1760
+ }
1761
+
1762
+ trackingService.trackInstallationOutcome('loop', loopName, 'success', { durationMs: Date.now() - startTime, batchId: options.batchId });
1763
+ return true;
1764
+
1765
+ } catch (error) {
1766
+ console.log(chalk.red(`āŒ Error installing loop: ${error.message}`));
1767
+ trackingService.trackInstallationOutcome('loop', loopName, 'failure', { errorType: 'network_error', errorMessage: error.message, durationMs: Date.now() - startTime, batchId: options.batchId });
1768
+ return false;
1769
+ }
1770
+ }
1771
+
1655
1772
  /**
1656
1773
  * Install multiple components with optional YAML workflow
1657
1774
  */
@@ -1666,7 +1783,8 @@ async function installMultipleComponents(options, targetDir) {
1666
1783
  mcps: [],
1667
1784
  settings: [],
1668
1785
  hooks: [],
1669
- skills: []
1786
+ skills: [],
1787
+ loops: []
1670
1788
  };
1671
1789
 
1672
1790
  // Parse comma-separated values for each component type
@@ -1700,7 +1818,12 @@ async function installMultipleComponents(options, targetDir) {
1700
1818
  components.skills = skillsInput.split(',').map(s => s.trim()).filter(s => s);
1701
1819
  }
1702
1820
 
1703
- const totalComponents = components.agents.length + components.commands.length + components.mcps.length + components.settings.length + components.hooks.length + components.skills.length;
1821
+ if (options.loop) {
1822
+ const loopsInput = Array.isArray(options.loop) ? options.loop.join(',') : options.loop;
1823
+ components.loops = loopsInput.split(',').map(l => l.trim()).filter(l => l);
1824
+ }
1825
+
1826
+ const totalComponents = components.agents.length + components.commands.length + components.mcps.length + components.settings.length + components.hooks.length + components.skills.length + components.loops.length;
1704
1827
 
1705
1828
  if (totalComponents === 0) {
1706
1829
  console.log(chalk.yellow('āš ļø No components specified to install.'));
@@ -1714,13 +1837,15 @@ async function installMultipleComponents(options, targetDir) {
1714
1837
  console.log(chalk.gray(` Settings: ${components.settings.length}`));
1715
1838
  console.log(chalk.gray(` Hooks: ${components.hooks.length}`));
1716
1839
  console.log(chalk.gray(` Skills: ${components.skills.length}`));
1717
-
1840
+ console.log(chalk.gray(` Loops: ${components.loops.length}`));
1841
+
1718
1842
  // Counter for successfully installed components
1719
1843
  let successfullyInstalled = 0;
1720
1844
 
1721
1845
  // Ask for installation locations once for configuration components (if any exist and not in silent mode)
1722
1846
  let sharedInstallLocations = ['local']; // default
1723
- const hasSettingsOrHooks = components.settings.length > 0 || components.hooks.length > 0;
1847
+ // Loops can pull in settings/hooks via their referenced components, so prompt for a location when loops are present too.
1848
+ const hasSettingsOrHooks = components.settings.length > 0 || components.hooks.length > 0 || components.loops.length > 0;
1724
1849
 
1725
1850
  if (hasSettingsOrHooks && !options.yes) {
1726
1851
  console.log(chalk.blue('\nšŸ“ Choose installation locations for configuration components:'));
@@ -1812,6 +1937,18 @@ async function installMultipleComponents(options, targetDir) {
1812
1937
  if (skillSuccess) successfullyInstalled++;
1813
1938
  }
1814
1939
 
1940
+ // Install loops (auto-installs their referenced components)
1941
+ for (const loop of components.loops) {
1942
+ console.log(chalk.gray(` Installing loop: ${loop}`));
1943
+ const loopSuccess = await installIndividualLoop(loop, targetDir, {
1944
+ ...options,
1945
+ silent: true,
1946
+ sharedInstallLocations: sharedInstallLocations,
1947
+ batchId
1948
+ });
1949
+ if (loopSuccess) successfullyInstalled++;
1950
+ }
1951
+
1815
1952
  // Handle YAML workflow if provided
1816
1953
  if (options.yaml) {
1817
1954
  console.log(chalk.blue('\nšŸ“„ Processing workflow YAML...'));
@@ -9,6 +9,12 @@ const fs = require('fs');
9
9
  const app = express();
10
10
  const PORT = process.env.PORT || 3444;
11
11
 
12
+ // Platform-aware command names. Now that spawns run without `shell: true`,
13
+ // Windows needs the `.cmd` shim to resolve npm-installed executables (bare
14
+ // 'npx'/'claude' would fail with ENOENT on win32).
15
+ const NPX_CMD = process.platform === 'win32' ? 'npx.cmd' : 'npx';
16
+ const CLAUDE_CMD = process.platform === 'win32' ? 'claude.cmd' : 'claude';
17
+
12
18
  // Load .env file from current working directory (where user runs the command)
13
19
  function loadEnvFile() {
14
20
  const envPath = path.join(process.cwd(), '.env');
@@ -47,11 +53,26 @@ function loadEnvFile() {
47
53
  // Load environment variables on startup
48
54
  const hasApiKeys = loadEnvFile();
49
55
 
50
- // Simple CORS middleware
56
+ // CORS middleware — restrict to the local Studio UI origin only.
57
+ // A wildcard (`*`) origin combined with the command-executing endpoints below
58
+ // lets any web page the developer visits drive requests into this server
59
+ // (drive-by RCE). Only allow the same-origin UI served from localhost:PORT.
60
+ const ALLOWED_ORIGINS = new Set([
61
+ `http://localhost:${PORT}`,
62
+ `http://127.0.0.1:${PORT}`,
63
+ ]);
51
64
  app.use((req, res, next) => {
52
- res.header('Access-Control-Allow-Origin', '*');
65
+ const origin = req.headers.origin;
66
+ if (origin && ALLOWED_ORIGINS.has(origin)) {
67
+ res.header('Access-Control-Allow-Origin', origin);
68
+ }
69
+ res.header('Vary', 'Origin');
53
70
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
54
71
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
72
+ // Reject cross-origin requests outright for the state-changing endpoints.
73
+ if (origin && !ALLOWED_ORIGINS.has(origin)) {
74
+ return res.status(403).json({ success: false, error: 'Cross-origin request rejected' });
75
+ }
55
76
  if (req.method === 'OPTIONS') {
56
77
  res.sendStatus(200);
57
78
  } else {
@@ -199,22 +220,31 @@ app.get('/api/tasks', (req, res) => {
199
220
  // API endpoint to install agent
200
221
  app.post('/api/install-agent', async (req, res) => {
201
222
  const { agentName } = req.body;
202
-
223
+
203
224
  if (!agentName) {
204
225
  return res.status(400).json({
205
226
  success: false,
206
227
  error: 'Agent name is required'
207
228
  });
208
229
  }
209
-
230
+
231
+ // SECURITY: agent names are `category/name` slugs. Reject anything else so a
232
+ // value like "x; rm -rf ~" can never reach the child process.
233
+ if (!/^[A-Za-z0-9._/-]+$/.test(agentName)) {
234
+ return res.status(400).json({
235
+ success: false,
236
+ error: 'Invalid agent name'
237
+ });
238
+ }
239
+
210
240
  try {
211
241
  console.log(chalk.blue('šŸ”§ Installing agent:'), chalk.cyan(agentName));
212
-
213
- // Use the CLI tool to install the agent
214
- const child = spawn('npx', ['claude-code-templates@latest', '--agent', agentName, '--yes'], {
242
+
243
+ // SECURITY: shell:false (default) keeps agentName as a single argv entry —
244
+ // no shell parses it, so metacharacters cannot inject commands.
245
+ const child = spawn(NPX_CMD, ['claude-code-templates@latest', '--agent', agentName, '--yes'], {
215
246
  cwd: process.cwd(),
216
- stdio: ['pipe', 'pipe', 'pipe'],
217
- shell: true
247
+ stdio: ['pipe', 'pipe', 'pipe']
218
248
  });
219
249
 
220
250
  let output = [];
@@ -276,11 +306,17 @@ async function checkAndInstallAgent(agentName, task) {
276
306
 
277
307
  task.output.push(`šŸ”§ Agent ${agentName} not found locally. Installing...`);
278
308
 
309
+ // SECURITY: reject anything that is not a plain `category/name` slug.
310
+ if (!/^[A-Za-z0-9._/-]+$/.test(agentName)) {
311
+ task.output.push(`āŒ Invalid agent name: ${agentName}`);
312
+ return Promise.resolve(false);
313
+ }
314
+
279
315
  return new Promise((resolve, reject) => {
280
- const child = spawn('npx', ['claude-code-templates@latest', '--agent', agentName, '--yes'], {
316
+ // SECURITY: shell:false (default) — agentName stays a single argv entry.
317
+ const child = spawn(NPX_CMD, ['claude-code-templates@latest', '--agent', agentName, '--yes'], {
281
318
  cwd: process.cwd(),
282
- stdio: ['pipe', 'pipe', 'pipe'],
283
- shell: true
319
+ stdio: ['pipe', 'pipe', 'pipe']
284
320
  });
285
321
 
286
322
  child.stdout.on('data', (data) => {
@@ -460,15 +496,18 @@ async function executeLocalTask(task) {
460
496
  finalPrompt = `As a ${task.agent.replace('-', ' ')}, ${task.prompt}`;
461
497
  }
462
498
 
463
- // Execute Claude Code locally with just the prompt
464
- const child = spawn('claude', [finalPrompt], {
499
+ // Execute Claude Code locally with just the prompt.
500
+ // SECURITY: never run through a shell. With shell:true, Node joins argv into
501
+ // a single `sh -c` string and the attacker-controlled prompt is parsed by the
502
+ // shell (command injection). shell:false keeps finalPrompt as a single argv[1].
503
+ // Windows: resolve claude.cmd explicitly (CLAUDE_CMD) instead of re-enabling shell:true.
504
+ const child = spawn(CLAUDE_CMD, [finalPrompt], {
465
505
  cwd: process.cwd(),
466
506
  stdio: ['ignore', 'pipe', 'pipe'], // Ignore stdin to prevent hanging
467
507
  env: {
468
508
  ...process.env,
469
509
  PATH: process.env.PATH
470
510
  },
471
- shell: true, // Use shell to find claude command
472
511
  timeout: 300000 // 5 minute timeout
473
512
  });
474
513
 
@@ -558,8 +597,9 @@ async function executeLocalTask(task) {
558
597
  }
559
598
  }
560
599
 
561
- // Start server
562
- app.listen(PORT, () => {
600
+ // Start server — bind to loopback only. This is a local developer tool that
601
+ // executes commands; binding to 0.0.0.0 exposes RCE to the whole LAN.
602
+ app.listen(PORT, '127.0.0.1', () => {
563
603
  console.log(chalk.blue('\\nšŸŽØ Claude Code Studio Server'));
564
604
  console.log(chalk.cyan('═══════════════════════════════════════'));
565
605
  console.log(chalk.green(`šŸš€ Server running on http://localhost:${PORT}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-templates",
3
- "version": "1.29.2",
3
+ "version": "1.29.4",
4
4
  "description": "Component templates and tracking system for Claude Code",
5
5
  "main": "cli-tool/src/index.js",
6
6
  "bin": {