@securityreviewai/securityreview-kit 0.1.38 → 0.1.40

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
@@ -28,7 +28,7 @@ npx @securityreviewai/securityreview-kit init --switch-project
28
28
  | Target | Flag | MCP Config | Workspace Rule |
29
29
  |---|---|---|---|
30
30
  | Cursor | `cursor` | `.cursor/mcp.json` | `.cursor/rules/srai-security-review.mdc`, `.cursor/rules/ctm_sync_rule.mdc`, `.cursor/commands/ctm_sync.md`, `.cursor/agents/ctm_sync.md`, `.cursor/commands/create-ide-workflow.md`, `.cursor/commands/srai-profile.md`, `.cursor/skills/threat-modelling/SKILL.md` |
31
- | Claude Code | `claude` | `.claude/settings.json` | `CLAUDE.md` |
31
+ | Claude Code | `claude` | `.claude/settings.json` | `CLAUDE.md`, `.claude/skills/threat-modelling/SKILL.md`, `.claude/skills/guardrails-profiler/SKILL.md`, `.claude/skills/guardrails-selection/SKILL.md`, `.claude/agents/ctm_sync.md`, `.claude/commands/guardrails-init-profile.md` |
32
32
  | VS Code Copilot | `vscode` | `.vscode/mcp.json` | `.github/copilot-instructions.md`, `.github/skills/threat-modelling/SKILL.md`, `.github/skills/guardrails-profiler/SKILL.md`, `.github/skills/guardrails-selection/SKILL.md`, `.github/agents/ctm_sync.agent.md`, `.github/hooks/srai-session-policy.json` |
33
33
  | Windsurf | `windsurf` | `.windsurf/mcp_config.json` | `.windsurf/rules/srai-security-review.md` |
34
34
  | Codex | `codex` | `.codex/config.toml` | `.codex/AGENTS.md`, `.codex/skills/threat-modelling/SKILL.md`, `.codex/skills/guardrails-profiler/SKILL.md`, `.codex/skills/guardrails-selection/SKILL.md`, `.codex/agents/ctm_sync.toml`, `.codex/hooks.json`, `.codex/commands/guardrails-init-profile.md` |
@@ -52,6 +52,15 @@ Options:
52
52
  --skip-mcp Skip MCP server config installation
53
53
  --skip-rules Skip workspace rule installation
54
54
  --profile-repo Run the guardrails profiler after init
55
+ --profiler-claude-login Run Claude Code login before profiling
56
+ --claude-auth-mode <mode>
57
+ Claude profiling auth mode: current, claudeai, console, api_key, gateway, bedrock, vertex, or setup_token
58
+ --claude-api-key <key> Anthropic API key for Claude profiling
59
+ --claude-base-url <url> Anthropic-compatible base URL for Claude profiling
60
+ --claude-auth-token <token>
61
+ Auth token for Claude profiling gateway mode
62
+ --claude-provider-model <model>
63
+ Optional Claude provider model override for gateway, Bedrock, or Vertex profiling
55
64
  --profiler-copilot-login
56
65
  Run GitHub Copilot CLI login before VS Code Copilot profiling
57
66
  --profiler-codex-login Run Codex login before Codex profiling
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securityreviewai/securityreview-kit",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
4
4
  "description": "Bootstrap security-review-mcp for AI IDEs and CLI tools",
5
5
  "author": "Debarshi Das <debarshi.das@we45.com>",
6
6
  "license": "UNLICENSED",
package/src/cli.js CHANGED
@@ -37,6 +37,30 @@ export function run() {
37
37
  '--profiler-cursor-login',
38
38
  'Before Cursor profiling, run `agent login` (or `cursor-agent login`) in this terminal (then profiling runs in the same init)',
39
39
  )
40
+ .option(
41
+ '--profiler-claude-login',
42
+ 'Before Claude Code profiling, run `claude auth login` in this terminal',
43
+ )
44
+ .option(
45
+ '--claude-auth-mode <mode>',
46
+ 'Claude profiling auth mode: current, claudeai, console, api_key, gateway, bedrock, vertex, or setup_token',
47
+ )
48
+ .option(
49
+ '--claude-api-key <key>',
50
+ 'Anthropic API key for Claude profiling when using --claude-auth-mode api_key',
51
+ )
52
+ .option(
53
+ '--claude-base-url <url>',
54
+ 'Anthropic-compatible base URL for Claude profiling when using --claude-auth-mode gateway',
55
+ )
56
+ .option(
57
+ '--claude-auth-token <token>',
58
+ 'Auth token for Claude profiling when using --claude-auth-mode gateway',
59
+ )
60
+ .option(
61
+ '--claude-provider-model <model>',
62
+ 'Optional Claude provider model override for gateway, Bedrock, or Vertex profiling',
63
+ )
40
64
  .option(
41
65
  '--profiler-copilot-login',
42
66
  'Before VS Code Copilot profiling, run `copilot login` in this terminal',
@@ -1,11 +1,13 @@
1
1
  import chalk from 'chalk';
2
- import { input, checkbox, confirm, select } from '@inquirer/prompts';
2
+ import { input, checkbox, confirm, password, select } from '@inquirer/prompts';
3
3
  import { TARGETS, TARGET_NAMES } from '../utils/constants.js';
4
4
  import { detectTargets } from '../utils/detect.js';
5
5
  import { ensureIdeClisForTargets } from '../utils/ide-cli-install.js';
6
6
  import { writeGuardrailsSkillBundles } from '../utils/guardrails-profiler-bundle.js';
7
7
  import {
8
8
  pickProfilerAgentTarget,
9
+ runClaudeAuthLogin,
10
+ runClaudeSetupToken,
9
11
  runCodexLogin,
10
12
  runCopilotLogin,
11
13
  runCursorAgentLogin,
@@ -52,6 +54,170 @@ function normalizeRuleResults(rawResult) {
52
54
  });
53
55
  }
54
56
 
57
+ async function resolveClaudeProfilerAuth(options, interactive, cwd) {
58
+ const providerModel = String(options.claudeProviderModel || process.env.ANTHROPIC_MODEL || '').trim();
59
+ let mode = String(options.claudeAuthMode || process.env.SECURITY_REVIEW_CLAUDE_AUTH_MODE || '').trim();
60
+
61
+ if (!mode && options.profilerClaudeLogin) {
62
+ mode = 'claudeai';
63
+ }
64
+
65
+ if (!mode && interactive) {
66
+ mode = await select({
67
+ message: 'How should Claude Code authenticate for this profiling run?',
68
+ default: 'current',
69
+ choices: [
70
+ { name: 'Use current Claude Code auth/environment', value: 'current' },
71
+ { name: 'Claude subscription login', value: 'claudeai' },
72
+ { name: 'Anthropic Console login', value: 'console' },
73
+ { name: 'Anthropic API key', value: 'api_key' },
74
+ { name: 'Anthropic-compatible gateway / proxy', value: 'gateway' },
75
+ { name: 'AWS Bedrock', value: 'bedrock' },
76
+ { name: 'Google Vertex AI', value: 'vertex' },
77
+ { name: 'Long-lived Claude token', value: 'setup_token' },
78
+ ],
79
+ });
80
+ }
81
+
82
+ if (!mode) {
83
+ mode = 'current';
84
+ }
85
+
86
+ const result = {
87
+ mode,
88
+ model: providerModel || 'haiku',
89
+ envOverrides: {},
90
+ loginRunner: null,
91
+ loginLabel: '',
92
+ summary: '',
93
+ };
94
+
95
+ if (mode === 'claudeai') {
96
+ result.loginRunner = () => runClaudeAuthLogin(cwd, { mode: 'claudeai' });
97
+ result.loginLabel = 'Claude subscription login';
98
+ result.summary = 'Claude subscription auth';
99
+ return result;
100
+ }
101
+
102
+ if (mode === 'console') {
103
+ result.loginRunner = () => runClaudeAuthLogin(cwd, { mode: 'console' });
104
+ result.loginLabel = 'Anthropic Console login';
105
+ result.summary = 'Anthropic Console auth';
106
+ return result;
107
+ }
108
+
109
+ if (mode === 'setup_token') {
110
+ result.loginRunner = () => runClaudeSetupToken(cwd);
111
+ result.loginLabel = 'Claude long-lived token setup';
112
+ result.summary = 'Claude subscription token auth';
113
+ return result;
114
+ }
115
+
116
+ if (mode === 'api_key') {
117
+ let apiKey = String(options.claudeApiKey || process.env.ANTHROPIC_API_KEY || '').trim();
118
+ if (!apiKey && interactive) {
119
+ apiKey = await password({
120
+ message: 'Anthropic API key for this profiling run:',
121
+ validate: (v) => (String(v || '').trim() ? true : 'API key is required'),
122
+ });
123
+ }
124
+ result.envOverrides = {
125
+ ANTHROPIC_API_KEY: apiKey,
126
+ ANTHROPIC_AUTH_TOKEN: null,
127
+ ANTHROPIC_BASE_URL: null,
128
+ CLAUDE_CODE_USE_BEDROCK: null,
129
+ CLAUDE_CODE_USE_VERTEX: null,
130
+ };
131
+ result.summary = 'Anthropic API key auth';
132
+ return result;
133
+ }
134
+
135
+ if (mode === 'gateway') {
136
+ let baseUrl = String(options.claudeBaseUrl || process.env.ANTHROPIC_BASE_URL || '').trim();
137
+ let authToken = String(options.claudeAuthToken || process.env.ANTHROPIC_AUTH_TOKEN || '').trim();
138
+ let model = providerModel;
139
+
140
+ if (interactive) {
141
+ if (!baseUrl) {
142
+ baseUrl = await input({
143
+ message: 'Anthropic-compatible base URL for this profiling run:',
144
+ validate: (v) => (String(v || '').trim() ? true : 'Base URL is required'),
145
+ });
146
+ }
147
+ if (!authToken) {
148
+ authToken = await password({
149
+ message: 'Gateway auth token for this profiling run:',
150
+ validate: (v) => (String(v || '').trim() ? true : 'Auth token is required'),
151
+ });
152
+ }
153
+ if (!providerModel) {
154
+ model = String(
155
+ await input({
156
+ message: 'Provider model override (leave blank to use haiku):',
157
+ default: '',
158
+ }),
159
+ ).trim();
160
+ }
161
+ }
162
+
163
+ result.model = model || 'haiku';
164
+ result.envOverrides = {
165
+ ANTHROPIC_BASE_URL: baseUrl,
166
+ ANTHROPIC_AUTH_TOKEN: authToken,
167
+ ANTHROPIC_API_KEY: null,
168
+ CLAUDE_CODE_USE_BEDROCK: null,
169
+ CLAUDE_CODE_USE_VERTEX: null,
170
+ };
171
+ result.summary = `Anthropic-compatible gateway (${baseUrl || 'configured base URL'})`;
172
+ return result;
173
+ }
174
+
175
+ if (mode === 'bedrock') {
176
+ let model = providerModel;
177
+ if (interactive && !providerModel) {
178
+ model = String(
179
+ await input({
180
+ message: 'Bedrock model override (leave blank to use haiku):',
181
+ default: '',
182
+ }),
183
+ ).trim();
184
+ }
185
+ result.model = model || 'haiku';
186
+ result.envOverrides = {
187
+ CLAUDE_CODE_USE_BEDROCK: 'true',
188
+ CLAUDE_CODE_USE_VERTEX: null,
189
+ ANTHROPIC_API_KEY: null,
190
+ ANTHROPIC_AUTH_TOKEN: null,
191
+ };
192
+ result.summary = 'AWS Bedrock credentials from your shell/environment';
193
+ return result;
194
+ }
195
+
196
+ if (mode === 'vertex') {
197
+ let model = providerModel;
198
+ if (interactive && !providerModel) {
199
+ model = String(
200
+ await input({
201
+ message: 'Vertex model override (leave blank to use haiku):',
202
+ default: '',
203
+ }),
204
+ ).trim();
205
+ }
206
+ result.model = model || 'haiku';
207
+ result.envOverrides = {
208
+ CLAUDE_CODE_USE_VERTEX: 'true',
209
+ CLAUDE_CODE_USE_BEDROCK: null,
210
+ ANTHROPIC_API_KEY: null,
211
+ ANTHROPIC_AUTH_TOKEN: null,
212
+ };
213
+ result.summary = 'Google Vertex AI credentials from your shell/environment';
214
+ return result;
215
+ }
216
+
217
+ result.summary = 'current Claude Code auth/environment';
218
+ return result;
219
+ }
220
+
55
221
  /**
56
222
  * Resolve environment variables from flags, env, or interactive prompt.
57
223
  */
@@ -417,6 +583,7 @@ export async function initCommand(options) {
417
583
  } else {
418
584
  console.log('');
419
585
  console.log(chalk.bold.white(` Starting profiler via ${TARGETS[agentTarget].name} CLI…`));
586
+ let claudeProfilerAuth = null;
420
587
  if (agentTarget === 'cursor') {
421
588
  console.log(
422
589
  chalk.dim(
@@ -486,6 +653,39 @@ export async function initCommand(options) {
486
653
  }
487
654
  console.log('');
488
655
  }
656
+ } else if (agentTarget === 'claude') {
657
+ claudeProfilerAuth = await resolveClaudeProfilerAuth(options, interactive, cwd);
658
+ console.log(
659
+ chalk.dim(
660
+ ` Claude Code: profiling uses \`.claude/settings.json\`, the configured MCP server, and \`${claudeProfilerAuth.model}\` for this run.`,
661
+ ),
662
+ );
663
+ console.log(chalk.dim(` Auth mode: ${claudeProfilerAuth.summary}.`));
664
+
665
+ const needsSetup = typeof claudeProfilerAuth.loginRunner === 'function';
666
+ let runLogin = needsSetup;
667
+ if (needsSetup && interactive && !options.claudeAuthMode && !options.profilerClaudeLogin) {
668
+ runLogin = await confirm({
669
+ message: `${claudeProfilerAuth.loginLabel} in this terminal now? (Same init — profiling runs next.)`,
670
+ default: true,
671
+ });
672
+ }
673
+ if (runLogin && claudeProfilerAuth.loginRunner) {
674
+ console.log('');
675
+ console.log(chalk.bold.white(` ${claudeProfilerAuth.loginLabel}`));
676
+ console.log(chalk.dim(' Complete the prompt flow, then return here.\n'));
677
+ const loginResult = claudeProfilerAuth.loginRunner();
678
+ if (loginResult.ok) {
679
+ console.log(chalk.green(` \u2713 ${claudeProfilerAuth.loginLabel} finished.`));
680
+ } else {
681
+ console.log(
682
+ chalk.yellow(
683
+ ` \u26a0 ${claudeProfilerAuth.loginLabel} exited with status ${loginResult.status ?? 'unknown'}. Profiling will still be attempted; configure auth and re-run init if it fails.`,
684
+ ),
685
+ );
686
+ }
687
+ console.log('');
688
+ }
489
689
  } else if (agentTarget === 'codex') {
490
690
  console.log(
491
691
  chalk.dim(
@@ -535,6 +735,8 @@ export async function initCommand(options) {
535
735
  projectName: projectNameForSkill,
536
736
  apiUrl: envVars?.apiUrl,
537
737
  apiToken: envVars?.apiToken,
738
+ modelOverride: agentTarget === 'claude' ? claudeProfilerAuth?.model : undefined,
739
+ extraEnv: agentTarget === 'claude' ? claudeProfilerAuth?.envOverrides : undefined,
538
740
  cursorTrust: !options.profilerNoTrust,
539
741
  streamProgress: profilerVerbose,
540
742
  showOutput: showProfilerOutput,
@@ -592,6 +794,34 @@ export async function initCommand(options) {
592
794
  ' • MCP missing: re-run init with VS Code selected and MCP installation enabled so `.vscode/mcp.json` is written.',
593
795
  ),
594
796
  );
797
+ } else if (agentTarget === 'claude') {
798
+ console.log('');
799
+ console.log(chalk.dim(' Typical fixes:'));
800
+ console.log(
801
+ chalk.dim(
802
+ ' • Choose the right auth mode: subscription, Console, API key, gateway, Bedrock, or Vertex.',
803
+ ),
804
+ );
805
+ console.log(
806
+ chalk.dim(
807
+ ' • CLI missing: install Claude Code and verify `claude --version`.',
808
+ ),
809
+ );
810
+ console.log(
811
+ chalk.dim(
812
+ ' • API key / gateway: provide `--claude-api-key`, or `--claude-base-url` plus `--claude-auth-token`, or set the matching env vars before re-running init.',
813
+ ),
814
+ );
815
+ console.log(
816
+ chalk.dim(
817
+ ' • Bedrock / Vertex: make sure your cloud credentials are already available in this shell before profiling.',
818
+ ),
819
+ );
820
+ console.log(
821
+ chalk.dim(
822
+ ' • MCP missing: re-run init with Claude Code selected and MCP installation enabled so `.claude/settings.json` is written.',
823
+ ),
824
+ );
595
825
  } else if (agentTarget === 'codex') {
596
826
  console.log('');
597
827
  console.log(chalk.dim(' Typical fixes:'));
@@ -2,6 +2,32 @@ import { join } from 'node:path';
2
2
  import { readJson, writeJson } from '../../utils/fs-helpers.js';
3
3
  import { MCP_SERVER_NAME, MCP_SERVER_PACKAGE } from '../../utils/constants.js';
4
4
 
5
+ function getClaudeSessionStartHooks() {
6
+ const prompt = [
7
+ 'MANDATORY SECURITY GATE (Claude Code Session Policy)',
8
+ '',
9
+ 'For any request with security impact, follow this order:',
10
+ '1. Fetch Vibe Guardrails first using .claude/skills/guardrails-selection/SKILL.md.',
11
+ '2. Run PWNISMS threat modelling using .claude/skills/threat-modelling/SKILL.md.',
12
+ '3. Implement secure code using the hydrated guardrails and threat findings.',
13
+ '4. Invoke the ctm_sync agent using .claude/agents/ctm_sync.md after implementation or threat-model updates.',
14
+ '',
15
+ 'Do not use project-profile exploration tools during normal coding tasks. No blocking and no deferral: guardrails first, PWNISMS second, implementation third, ctm_sync last.',
16
+ ].join('\n');
17
+
18
+ return [
19
+ {
20
+ matcher: '.*',
21
+ hooks: [
22
+ {
23
+ type: 'prompt',
24
+ prompt,
25
+ },
26
+ ],
27
+ },
28
+ ];
29
+ }
30
+
5
31
  /**
6
32
  * Generate Claude Code MCP config at .claude/settings.json
7
33
  */
@@ -22,6 +48,15 @@ export function generate(cwd, envVars) {
22
48
  },
23
49
  };
24
50
 
51
+ const existingSessionStart = Array.isArray(existing.SessionStart) ? existing.SessionStart : [];
52
+ const marker = 'MANDATORY SECURITY GATE (Claude Code Session Policy)';
53
+ const ours = getClaudeSessionStartHooks();
54
+ const hasOurs = existingSessionStart.some((entry) =>
55
+ Array.isArray(entry?.hooks) &&
56
+ entry.hooks.some((hook) => hook?.type === 'prompt' && typeof hook?.prompt === 'string' && hook.prompt.includes(marker)),
57
+ );
58
+ existing.SessionStart = hasOurs ? existingSessionStart : [...existingSessionStart, ...ours];
59
+
25
60
  writeJson(filePath, existing);
26
61
  return filePath;
27
62
  }
@@ -0,0 +1,53 @@
1
+ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { test } from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import { generate } from './claude.js';
7
+
8
+ test('Claude MCP generator writes mcp server and session hooks', () => {
9
+ const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-claude-mcp-'));
10
+
11
+ generate(cwd, {
12
+ apiUrl: 'https://example.test',
13
+ apiToken: 'secret-token',
14
+ });
15
+
16
+ const config = JSON.parse(readFileSync(join(cwd, '.claude', 'settings.json'), 'utf8'));
17
+ assert.equal(config.mcpServers['security-review-mcp'].command, 'npx');
18
+ assert.equal(Array.isArray(config.SessionStart), true);
19
+ assert.match(config.SessionStart[0].hooks[0].prompt, /MANDATORY SECURITY GATE/);
20
+ });
21
+
22
+ test('Claude MCP generator preserves existing SessionStart hooks', () => {
23
+ const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-claude-mcp-merge-'));
24
+ const configPath = join(cwd, '.claude', 'settings.json');
25
+ mkdirSync(join(cwd, '.claude'), { recursive: true });
26
+
27
+ writeFileSync(
28
+ configPath,
29
+ JSON.stringify(
30
+ {
31
+ SessionStart: [
32
+ {
33
+ matcher: '.*',
34
+ hooks: [{ type: 'prompt', prompt: 'Existing hook' }],
35
+ },
36
+ ],
37
+ },
38
+ null,
39
+ 2,
40
+ ),
41
+ 'utf8',
42
+ );
43
+
44
+ generate(cwd, {
45
+ apiUrl: 'https://example.test',
46
+ apiToken: 'secret-token',
47
+ });
48
+
49
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
50
+ assert.equal(config.SessionStart.length, 2);
51
+ assert.match(config.SessionStart[0].hooks[0].prompt, /Existing hook/);
52
+ assert.match(config.SessionStart[1].hooks[0].prompt, /MANDATORY SECURITY GATE/);
53
+ });
@@ -1,28 +1,83 @@
1
+ import { existsSync } from 'node:fs';
1
2
  import { join } from 'node:path';
2
- import { GUARDRAILS_PROFILER_SKILL_REL_DIR, GUARDRAILS_SELECTION_SKILL_REL_DIR } from '../../utils/constants.js';
3
+ import {
4
+ CTM_SYNC_AGENT_REL_PATH,
5
+ GUARDRAILS_PROFILER_SKILL_REL_DIR,
6
+ GUARDRAILS_SELECTION_SKILL_REL_DIR,
7
+ THREAT_MODELLING_SKILL_REL_DIR,
8
+ } from '../../utils/constants.js';
3
9
  import { upsertSentinelBlock, writeText } from '../../utils/fs-helpers.js';
4
- import { getRuleContent, getGuardrailsInitProfileContent } from './content.js';
10
+ import {
11
+ getCtmSyncWorkflowContent,
12
+ getGuardrailsInitProfileContent,
13
+ getRuleContent,
14
+ getThreatModellingSkillContent,
15
+ } from './content.js';
16
+
17
+ function writeGeneratedText(filePath, content) {
18
+ const action = existsSync(filePath) ? 'updated' : 'created';
19
+ writeText(filePath, content);
20
+ return { filePath, action };
21
+ }
22
+
23
+ function getAgentBody(content) {
24
+ return content.replace(/^---\n[\s\S]*?\n---\n*/, '').trim();
25
+ }
26
+
27
+ function getClaudeCtmSyncAgentContent(options = {}) {
28
+ const body = getAgentBody(getCtmSyncWorkflowContent(options));
29
+ return [
30
+ '---',
31
+ 'name: ctm_sync',
32
+ 'description: Use this agent after security-relevant implementation or threat modelling work to synchronize the latest threat model, mitigations, and guardrails to SRAI.',
33
+ 'model: inherit',
34
+ 'color: blue',
35
+ '---',
36
+ '',
37
+ body,
38
+ '',
39
+ ].join('\n');
40
+ }
5
41
 
6
42
  /**
7
43
  * Generate Claude Code workspace rule — appends to CLAUDE.md
8
44
  */
9
45
  export function generate(cwd, options = {}) {
10
- const filePath = join(cwd, 'CLAUDE.md');
11
- const content = getRuleContent({
46
+ const optionsWithSkillDirs = {
12
47
  ...options,
13
48
  guardrailsSelectionSkillDir: GUARDRAILS_SELECTION_SKILL_REL_DIR.claude,
14
- });
49
+ threatModellingSkillDir: THREAT_MODELLING_SKILL_REL_DIR.claude,
50
+ ctmSyncAgentPath: CTM_SYNC_AGENT_REL_PATH.claude,
51
+ };
52
+ const filePath = join(cwd, 'CLAUDE.md');
53
+ const content = getRuleContent(optionsWithSkillDirs);
15
54
  const action = upsertSentinelBlock(filePath, content);
16
55
 
56
+ const threatSkillPath = join(cwd, THREAT_MODELLING_SKILL_REL_DIR.claude, 'SKILL.md');
57
+ const threatSkill = writeGeneratedText(
58
+ threatSkillPath,
59
+ getThreatModellingSkillContent(optionsWithSkillDirs),
60
+ );
61
+
62
+ const ctmSyncAgentPath = join(cwd, CTM_SYNC_AGENT_REL_PATH.claude);
63
+ const ctmSyncAgent = writeGeneratedText(
64
+ ctmSyncAgentPath,
65
+ getClaudeCtmSyncAgentContent(optionsWithSkillDirs),
66
+ );
67
+
17
68
  const guardrailsInitPath = join(cwd, '.claude', 'commands', 'guardrails-init-profile.md');
18
- const guardrailsInitContent = getGuardrailsInitProfileContent({
19
- ...options,
20
- guardrailsSkillDir: GUARDRAILS_PROFILER_SKILL_REL_DIR.claude,
21
- });
22
- writeText(guardrailsInitPath, guardrailsInitContent);
69
+ const guardrailsInit = writeGeneratedText(
70
+ guardrailsInitPath,
71
+ getGuardrailsInitProfileContent({
72
+ ...optionsWithSkillDirs,
73
+ guardrailsSkillDir: GUARDRAILS_PROFILER_SKILL_REL_DIR.claude,
74
+ }),
75
+ );
23
76
 
24
77
  return [
25
78
  { filePath, action, kind: 'rule' },
26
- { filePath: guardrailsInitPath, action: 'created', kind: 'command' },
79
+ { ...threatSkill, kind: 'skill' },
80
+ { ...ctmSyncAgent, kind: 'agent' },
81
+ { ...guardrailsInit, kind: 'command' },
27
82
  ];
28
83
  }
@@ -0,0 +1,39 @@
1
+ import { existsSync, mkdtempSync, readFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { test } from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import { generate } from './claude.js';
7
+
8
+ test('Claude generator writes CLAUDE.md, threat skill, ctm_sync agent, and profiling command', () => {
9
+ const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-claude-'));
10
+
11
+ const results = generate(cwd, { projectName: 'SmokeProject' });
12
+
13
+ const expectedPaths = [
14
+ 'CLAUDE.md',
15
+ '.claude/skills/threat-modelling/SKILL.md',
16
+ '.claude/agents/ctm_sync.md',
17
+ '.claude/commands/guardrails-init-profile.md',
18
+ ];
19
+
20
+ for (const relPath of expectedPaths) {
21
+ assert.equal(existsSync(join(cwd, relPath)), true, `${relPath} should exist`);
22
+ }
23
+
24
+ assert.deepEqual(results.map((entry) => entry.kind), ['rule', 'skill', 'agent', 'command']);
25
+
26
+ const instructions = readFileSync(join(cwd, 'CLAUDE.md'), 'utf8');
27
+ assert.match(instructions, /\.claude\/skills\/guardrails-selection\/SKILL\.md/);
28
+ assert.match(instructions, /\.claude\/skills\/threat-modelling\/SKILL\.md/);
29
+ assert.match(instructions, /\.claude\/agents\/ctm_sync\.md/);
30
+
31
+ const threatSkill = readFileSync(join(cwd, '.claude/skills/threat-modelling/SKILL.md'), 'utf8');
32
+ assert.match(threatSkill, /\.claude\/agents\/ctm_sync\.md/);
33
+ assert.doesNotMatch(threatSkill, /get_project_profile_description/);
34
+
35
+ const agent = readFileSync(join(cwd, '.claude/agents/ctm_sync.md'), 'utf8');
36
+ assert.match(agent, /^---\nname: ctm_sync/m);
37
+ assert.match(agent, /model: inherit/);
38
+ assert.match(agent, /Configured SRAI project name: `SmokeProject`/);
39
+ });
@@ -31,6 +31,16 @@ During `securityreview-kit init`, choose **Yes** when asked to run Cursor login
31
31
 
32
32
  You can still sign in manually with `agent login` (or `cursor-agent login`). To handle trust/login interactively in the terminal, omit `--trust` and `--approve-mcps`.
33
33
 
34
+ ## Claude Code CLI (scripted)
35
+
36
+ From the repo root, non-interactive runs should execute with the project settings file and the Haiku model for this profiling pass:
37
+
38
+ `claude -p "<your profiling instructions>" --settings .claude/settings.json --model haiku`
39
+
40
+ During `securityreview-kit init`, choose **Yes** when asked to run Claude Code login, or pass **`--profiler-claude-login`** with **`--profile-repo`** so `claude auth login` and profiling stay in one run.
41
+
42
+ Claude profiling can also run with **Anthropic Console**, **ANTHROPIC_API_KEY**, an **Anthropic-compatible gateway** (`ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN`), or cloud-provider credentials such as **AWS Bedrock** and **Google Vertex AI**. `securityreview-kit init` can branch into those auth modes before profiling.
43
+
34
44
  ## GitHub Copilot CLI (scripted)
35
45
 
36
46
  From the repo root, non-interactive runs should load the SRAI MCP server and allow the tools needed to scan, write profile files, and call MCP:
@@ -78,6 +78,7 @@ export const GUARDRAILS_SELECTION_SKILL_REL_DIR = {
78
78
  /** Relative workspace dirs for the PWNISMS threat-modelling skill (per IDE / CLI). */
79
79
  export const THREAT_MODELLING_SKILL_REL_DIR = {
80
80
  cursor: '.cursor/skills/threat-modelling',
81
+ claude: '.claude/skills/threat-modelling',
81
82
  vscode: '.github/skills/threat-modelling',
82
83
  codex: '.codex/skills/threat-modelling',
83
84
  };
@@ -85,6 +86,7 @@ export const THREAT_MODELLING_SKILL_REL_DIR = {
85
86
  /** Relative workspace paths for the CTM sync agent/workflow (per IDE / CLI). */
86
87
  export const CTM_SYNC_AGENT_REL_PATH = {
87
88
  cursor: '.cursor/agents/ctm_sync.md',
89
+ claude: '.claude/agents/ctm_sync.md',
88
90
  vscode: '.github/agents/ctm_sync.agent.md',
89
91
  codex: '.codex/agents/ctm_sync.toml',
90
92
  };
@@ -15,8 +15,15 @@ export function getProfilerLogPath(cwd, target) {
15
15
  return join(cwd, '.guardrails', 'logs', `profiler-${target}.log`);
16
16
  }
17
17
 
18
- function buildProfilerEnv(target, streamProgress) {
18
+ function buildProfilerEnv(target, streamProgress, extraEnv = {}) {
19
19
  const env = augmentPathEnv(process.env);
20
+ for (const [key, value] of Object.entries(extraEnv || {})) {
21
+ if (value == null || value === '') {
22
+ delete env[key];
23
+ } else {
24
+ env[key] = value;
25
+ }
26
+ }
20
27
  if (target === 'codex' && streamProgress && !env.RUST_LOG) {
21
28
  env.RUST_LOG = 'info';
22
29
  }
@@ -109,6 +116,54 @@ export function runCopilotLogin(cwd) {
109
116
  return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
110
117
  }
111
118
 
119
+ /**
120
+ * Run Claude Code login in the current terminal.
121
+ */
122
+ export function runClaudeLogin(cwd) {
123
+ return runClaudeAuthLogin(cwd, { mode: 'claudeai' });
124
+ }
125
+
126
+ /**
127
+ * Run Claude Code auth login in the current terminal.
128
+ */
129
+ export function runClaudeAuthLogin(cwd, options = {}) {
130
+ const env = augmentPathEnv(process.env);
131
+ if (!commandOk('claude', ['--version'], env)) {
132
+ return {
133
+ ok: false,
134
+ status: null,
135
+ message: 'Claude Code CLI not found (`claude`). Install from https://claude.ai/code.',
136
+ };
137
+ }
138
+ const mode = options.mode === 'console' ? '--console' : '--claudeai';
139
+ const r = spawnSync('claude', ['auth', 'login', mode], { cwd, stdio: 'inherit', env });
140
+ const spawnErr = r.error ? r.error.message : null;
141
+ if (r.status === null && spawnErr) {
142
+ return { ok: false, status: null, message: spawnErr };
143
+ }
144
+ return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
145
+ }
146
+
147
+ /**
148
+ * Run Claude Code long-lived token setup in the current terminal.
149
+ */
150
+ export function runClaudeSetupToken(cwd) {
151
+ const env = augmentPathEnv(process.env);
152
+ if (!commandOk('claude', ['--version'], env)) {
153
+ return {
154
+ ok: false,
155
+ status: null,
156
+ message: 'Claude Code CLI not found (`claude`). Install from https://claude.ai/code.',
157
+ };
158
+ }
159
+ const r = spawnSync('claude', ['setup-token'], { cwd, stdio: 'inherit', env });
160
+ const spawnErr = r.error ? r.error.message : null;
161
+ if (r.status === null && spawnErr) {
162
+ return { ok: false, status: null, message: spawnErr };
163
+ }
164
+ return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
165
+ }
166
+
112
167
  /**
113
168
  * Run Codex login in the current terminal. Device auth keeps the flow in-terminal.
114
169
  */
@@ -185,13 +240,15 @@ export function runProfilerAgent(
185
240
  projectName,
186
241
  apiUrl,
187
242
  apiToken,
243
+ modelOverride,
244
+ extraEnv,
188
245
  cursorTrust = true,
189
246
  streamProgress = false,
190
247
  showOutput = streamProgress,
191
248
  },
192
249
  ) {
193
250
  const prompt = buildProfilerAgentPrompt(projectName, target);
194
- const env = buildProfilerEnv(target, streamProgress);
251
+ const env = buildProfilerEnv(target, streamProgress, extraEnv);
195
252
  const opts = showOutput
196
253
  ? { cwd, stdio: 'inherit', env }
197
254
  : { cwd, stdio: ['ignore', 'pipe', 'pipe'], env, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 };
@@ -230,9 +287,12 @@ export function runProfilerAgent(
230
287
  if (!commandOk('claude', ['--version'], env)) {
231
288
  return { ok: false, message: 'claude not on PATH' };
232
289
  }
233
- const args = streamProgress
234
- ? ['-p', '--output-format', 'stream-json', '--include-partial-messages', '--verbose', prompt]
235
- : ['-p', prompt];
290
+ const settingsPath = join(cwd, '.claude', 'settings.json');
291
+ const args = ['-p', '--settings', settingsPath, '--model', modelOverride || 'haiku'];
292
+ if (streamProgress) {
293
+ args.push('--output-format', 'stream-json', '--include-partial-messages', '--include-hook-events', '--verbose');
294
+ }
295
+ args.push(prompt);
236
296
  const r = spawnSync('claude', args, opts);
237
297
  return buildProfilerResult(r, { logPath: showOutput ? null : persistProfilerLog(cwd, target, r) });
238
298
  }