@securityreviewai/securityreview-kit 0.1.37 → 0.1.39

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,10 +28,10 @@ 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
- | Codex | `codex` | `.codex/config.toml` | `AGENTS.md`, `.agents/skills/threat-modelling/SKILL.md`, `.agents/skills/guardrails-profiler/SKILL.md`, `.agents/skills/guardrails-selection/SKILL.md`, `.codex/agents/ctm_sync.toml`, `.codex/hooks.json`, `.codex/commands/guardrails-init-profile.md` |
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` |
35
35
  | Gemini CLI | `gemini` | `.gemini/settings.json` | `GEMINI.md` |
36
36
  | Antigravity | `antigravity` | `.gemini/settings.json` | `.agents/rules/srai-security-review.md` |
37
37
 
@@ -52,8 +52,12 @@ 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
55
56
  --profiler-copilot-login
56
57
  Run GitHub Copilot CLI login before VS Code Copilot profiling
58
+ --profiler-codex-login Run Codex login before Codex profiling
59
+ --profiler-verbose Show live profiler output while profiling runs
60
+ --show-profiler-logs Alias for --profiler-verbose
57
61
  ```
58
62
 
59
63
  ### `@securityreviewai/securityreview-kit init --switch-project`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securityreviewai/securityreview-kit",
3
- "version": "0.1.37",
3
+ "version": "0.1.39",
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,10 +37,18 @@ 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
+ )
40
44
  .option(
41
45
  '--profiler-copilot-login',
42
46
  'Before VS Code Copilot profiling, run `copilot login` in this terminal',
43
47
  )
48
+ .option(
49
+ '--profiler-codex-login',
50
+ 'Before Codex profiling, run `codex login --device-auth` in this terminal',
51
+ )
44
52
  .option(
45
53
  '--profiler-quiet',
46
54
  'When profiling, use the standard progress message (default; retained for compatibility)',
@@ -49,6 +57,10 @@ export function run() {
49
57
  '--profiler-verbose',
50
58
  'When profiling, show live agent output for troubleshooting',
51
59
  )
60
+ .option(
61
+ '--show-profiler-logs',
62
+ 'Alias for --profiler-verbose; show live profiler logs while profiling runs',
63
+ )
52
64
  .action(async (options) => {
53
65
  try {
54
66
  if (options.switchProject) {
@@ -6,6 +6,8 @@ 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
+ runClaudeLogin,
10
+ runCodexLogin,
9
11
  runCopilotLogin,
10
12
  runCursorAgentLogin,
11
13
  runProfilerAgent,
@@ -485,27 +487,95 @@ export async function initCommand(options) {
485
487
  }
486
488
  console.log('');
487
489
  }
490
+ } else if (agentTarget === 'claude') {
491
+ console.log(
492
+ chalk.dim(
493
+ ' Claude Code: profiling uses `.claude/settings.json`, the configured MCP server, and the Haiku model for this run.',
494
+ ),
495
+ );
496
+
497
+ let runLogin = Boolean(options.profilerClaudeLogin);
498
+ if (!runLogin && interactive) {
499
+ runLogin = await confirm({
500
+ message:
501
+ 'Run Claude Code login in this terminal now? (Same init — profiling runs next. Choose No if already signed in.)',
502
+ default: true,
503
+ });
504
+ }
505
+ if (runLogin) {
506
+ console.log('');
507
+ console.log(chalk.bold.white(' Claude Code login'));
508
+ console.log(chalk.dim(' Complete the browser prompt, then return here.\n'));
509
+ const loginResult = runClaudeLogin(cwd);
510
+ if (loginResult.ok) {
511
+ console.log(chalk.green(' \u2713 Claude Code login step finished.'));
512
+ } else {
513
+ console.log(
514
+ chalk.yellow(
515
+ ` \u26a0 Claude login exited with status ${loginResult.status ?? 'unknown'}. Profiling will still be attempted; sign in and re-run init if it fails.`,
516
+ ),
517
+ );
518
+ }
519
+ console.log('');
520
+ }
521
+ } else if (agentTarget === 'codex') {
522
+ console.log(
523
+ chalk.dim(
524
+ ' Codex: profiling passes the SRAI MCP server directly to `codex exec` for this run.',
525
+ ),
526
+ );
527
+
528
+ let runLogin = Boolean(options.profilerCodexLogin);
529
+ if (!runLogin && interactive) {
530
+ runLogin = await confirm({
531
+ message:
532
+ 'Run Codex login in this terminal now? (Same init — profiling runs next. Choose No if already signed in.)',
533
+ default: true,
534
+ });
535
+ }
536
+ if (runLogin) {
537
+ console.log('');
538
+ console.log(chalk.bold.white(' Codex login'));
539
+ console.log(chalk.dim(' Complete the device-code prompt, then return here.\n'));
540
+ const loginResult = runCodexLogin(cwd);
541
+ if (loginResult.ok) {
542
+ console.log(chalk.green(' \u2713 Codex login step finished.'));
543
+ } else {
544
+ console.log(
545
+ chalk.yellow(
546
+ ` \u26a0 Codex login exited with status ${loginResult.status ?? 'unknown'}. Profiling will still be attempted; sign in and re-run init if it fails.`,
547
+ ),
548
+ );
549
+ }
550
+ console.log('');
551
+ }
488
552
  } else {
489
553
  console.log(chalk.dim(' (Sign-in or approvals may be required in your terminal.)'));
490
554
  }
491
555
  console.log('');
556
+ const profilerVerbose = Boolean(options.profilerVerbose || options.showProfilerLogs);
492
557
  const showProfilerOutput = Boolean(
493
- options.profilerVerbose || (agentTarget === 'cursor' && options.profilerNoTrust),
558
+ profilerVerbose || (agentTarget === 'cursor' && options.profilerNoTrust),
494
559
  );
495
560
  if (showProfilerOutput) {
496
- console.log(chalk.dim(' Profiling in progress. Agent output is visible for this run...'));
561
+ console.log(chalk.dim(' Profiling in progress. Live profiler logs are visible for this run...'));
497
562
  } else {
498
563
  console.log(chalk.dim(' Profiling in progress. This can take a few minutes...'));
499
564
  }
500
565
  const pr = runProfilerAgent(cwd, {
501
566
  target: agentTarget,
502
567
  projectName: projectNameForSkill,
568
+ apiUrl: envVars?.apiUrl,
569
+ apiToken: envVars?.apiToken,
503
570
  cursorTrust: !options.profilerNoTrust,
504
- streamProgress: Boolean(options.profilerVerbose),
571
+ streamProgress: profilerVerbose,
505
572
  showOutput: showProfilerOutput,
506
573
  });
507
574
  if (pr.ok) {
508
575
  console.log(chalk.green(' \u2713 Profiler agent finished.'));
576
+ if (pr.logPath) {
577
+ console.log(chalk.dim(` Profiler log saved to ${pr.logPath}`));
578
+ }
509
579
  } else {
510
580
  const detail =
511
581
  pr.message ||
@@ -515,6 +585,9 @@ export async function initCommand(options) {
515
585
  ` \u26a0 Profiler agent exited with an error: ${detail}. You can run the guardrails-init-profile workflow manually.`,
516
586
  ),
517
587
  );
588
+ if (pr.logPath) {
589
+ console.log(chalk.dim(` Profiler log saved to ${pr.logPath}`));
590
+ }
518
591
  if (agentTarget === 'cursor') {
519
592
  console.log('');
520
593
  console.log(chalk.dim(' Typical fixes:'));
@@ -551,6 +624,42 @@ export async function initCommand(options) {
551
624
  ' • MCP missing: re-run init with VS Code selected and MCP installation enabled so `.vscode/mcp.json` is written.',
552
625
  ),
553
626
  );
627
+ } else if (agentTarget === 'claude') {
628
+ console.log('');
629
+ console.log(chalk.dim(' Typical fixes:'));
630
+ console.log(
631
+ chalk.dim(
632
+ ' • Not signed in: re-run `securityreview-kit init` and choose Yes for Claude Code login, or pass `--profiler-claude-login` with `--profile-repo`.',
633
+ ),
634
+ );
635
+ console.log(
636
+ chalk.dim(
637
+ ' • CLI missing: install Claude Code and verify `claude --version`.',
638
+ ),
639
+ );
640
+ console.log(
641
+ chalk.dim(
642
+ ' • MCP missing: re-run init with Claude Code selected and MCP installation enabled so `.claude/settings.json` is written.',
643
+ ),
644
+ );
645
+ } else if (agentTarget === 'codex') {
646
+ console.log('');
647
+ console.log(chalk.dim(' Typical fixes:'));
648
+ console.log(
649
+ chalk.dim(
650
+ ' • Not signed in: re-run `securityreview-kit init` and choose Yes for Codex login, or pass `--profiler-codex-login` with `--profile-repo`.',
651
+ ),
652
+ );
653
+ console.log(
654
+ chalk.dim(
655
+ ' • CLI missing: install Codex CLI and verify `codex --version`.',
656
+ ),
657
+ );
658
+ console.log(
659
+ chalk.dim(
660
+ ' • MCP missing: re-run init with Codex selected and MCP installation enabled so the SRAI credentials are available for profiling.',
661
+ ),
662
+ );
554
663
  }
555
664
  }
556
665
  }
@@ -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
+ });
@@ -26,20 +26,26 @@ function getCodexSessionHookContent() {
26
26
  '',
27
27
  'For any request with security impact (auth, authorization, crypto, input handling, secrets, network, data storage, dependencies, new APIs/endpoints, infrastructure, or code handling untrusted data), follow this order:',
28
28
  '',
29
- '1. Fetch Vibe Guardrails first using .agents/skills/guardrails-selection/SKILL.md. Resolve the project, call get_guardrails, shortlist relevant guardrails, then call get_guardrail_by_id for the shortlist.',
30
- '2. Run PWNISMS threat modelling using .agents/skills/threat-modelling/SKILL.md. Explicitly walk Product, Workload, Network, IAM, Secrets, Monitoring, and Supply Chain.',
29
+ '1. Fetch Vibe Guardrails first using .codex/skills/guardrails-selection/SKILL.md. Resolve the project, call get_guardrails, shortlist relevant guardrails, then call get_guardrail_by_id for the shortlist.',
30
+ '2. Run PWNISMS threat modelling using .codex/skills/threat-modelling/SKILL.md. Explicitly walk Product, Workload, Network, IAM, Secrets, Monitoring, and Supply Chain.',
31
31
  '3. Implement secure code using both the hydrated guardrails and PWNISMS findings.',
32
32
  '4. Invoke the ctm_sync subagent using .codex/agents/ctm_sync.toml after threat modelling or guardrail enforcement. Reuse the exact guardrail shortlist; do not re-query guardrails during CTM sync.',
33
33
  '',
34
34
  '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.',
35
35
  ].join('\n');
36
36
 
37
- const commandPayload = JSON.stringify({
37
+ const sessionStartPayload = JSON.stringify({
38
38
  hookSpecificOutput: {
39
39
  hookEventName: 'SessionStart',
40
40
  additionalContext,
41
41
  },
42
42
  });
43
+ const userPromptPayload = JSON.stringify({
44
+ hookSpecificOutput: {
45
+ hookEventName: 'UserPromptSubmit',
46
+ additionalContext,
47
+ },
48
+ });
43
49
 
44
50
  return JSON.stringify(
45
51
  {
@@ -50,13 +56,25 @@ function getCodexSessionHookContent() {
50
56
  hooks: [
51
57
  {
52
58
  type: 'command',
53
- command: `printf '%s\\n' '${commandPayload.replaceAll("'", "'\\''")}'`,
59
+ command: `printf '%s\\n' '${sessionStartPayload.replaceAll("'", "'\\''")}'`,
54
60
  timeout: 5,
55
61
  statusMessage: 'Loading SRAI security session policy',
56
62
  },
57
63
  ],
58
64
  },
59
65
  ],
66
+ UserPromptSubmit: [
67
+ {
68
+ hooks: [
69
+ {
70
+ type: 'command',
71
+ command: `printf '%s\\n' '${userPromptPayload.replaceAll("'", "'\\''")}'`,
72
+ timeout: 5,
73
+ statusMessage: 'Refreshing SRAI security session policy',
74
+ },
75
+ ],
76
+ },
77
+ ],
60
78
  },
61
79
  },
62
80
  null,
@@ -90,7 +108,7 @@ export function generate(cwd, options = {}) {
90
108
  threatModellingSkillDir: THREAT_MODELLING_SKILL_REL_DIR.codex,
91
109
  ctmSyncAgentPath: CTM_SYNC_AGENT_REL_PATH.codex,
92
110
  };
93
- const filePath = join(cwd, 'AGENTS.md');
111
+ const filePath = join(cwd, '.codex', 'AGENTS.md');
94
112
  const content = getRuleContent(optionsWithSkillDirs);
95
113
  const action = upsertSentinelBlock(filePath, content);
96
114
 
@@ -11,8 +11,8 @@ test('Codex generator writes AGENTS, skills, subagent, hooks, and profiling comm
11
11
  const results = generate(cwd, { projectName: 'SmokeProject' });
12
12
 
13
13
  const expectedPaths = [
14
- 'AGENTS.md',
15
- '.agents/skills/threat-modelling/SKILL.md',
14
+ '.codex/AGENTS.md',
15
+ '.codex/skills/threat-modelling/SKILL.md',
16
16
  '.codex/agents/ctm_sync.toml',
17
17
  '.codex/hooks.json',
18
18
  '.codex/commands/guardrails-init-profile.md',
@@ -24,13 +24,14 @@ test('Codex generator writes AGENTS, skills, subagent, hooks, and profiling comm
24
24
 
25
25
  assert.deepEqual(results.map((entry) => entry.kind), ['rule', 'skill', 'agent', 'hooks', 'command']);
26
26
 
27
- const instructions = readFileSync(join(cwd, 'AGENTS.md'), 'utf8');
28
- assert.match(instructions, /\.agents\/skills\/guardrails-selection\/SKILL\.md/);
29
- assert.match(instructions, /\.agents\/skills\/threat-modelling\/SKILL\.md/);
27
+ const instructions = readFileSync(join(cwd, '.codex/AGENTS.md'), 'utf8');
28
+ assert.match(instructions, /\.codex\/skills\/guardrails-selection\/SKILL\.md/);
29
+ assert.match(instructions, /\.codex\/skills\/threat-modelling\/SKILL\.md/);
30
30
  assert.match(instructions, /\.codex\/agents\/ctm_sync\.toml/);
31
31
  assert.doesNotMatch(instructions, /\.cursor/);
32
+ assert.doesNotMatch(instructions, /\.agents\/skills/);
32
33
 
33
- const threatSkill = readFileSync(join(cwd, '.agents/skills/threat-modelling/SKILL.md'), 'utf8');
34
+ const threatSkill = readFileSync(join(cwd, '.codex/skills/threat-modelling/SKILL.md'), 'utf8');
34
35
  for (const heading of ['Product', 'Workload', 'Network', 'IAM', 'Secrets', 'Monitoring', 'Supply Chain']) {
35
36
  assert.match(threatSkill, new RegExp(heading));
36
37
  }
@@ -44,4 +45,6 @@ test('Codex generator writes AGENTS, skills, subagent, hooks, and profiling comm
44
45
 
45
46
  const hooks = JSON.parse(readFileSync(join(cwd, '.codex/hooks.json'), 'utf8'));
46
47
  assert.equal(hooks.hooks.SessionStart[0].hooks[0].type, 'command');
48
+ assert.equal(hooks.hooks.UserPromptSubmit[0].hooks[0].type, 'command');
49
+ assert.match(hooks.hooks.UserPromptSubmit[0].hooks[0].command, /"hookEventName":"UserPromptSubmit"/);
47
50
  });
@@ -31,6 +31,14 @@ 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
+
34
42
  ## GitHub Copilot CLI (scripted)
35
43
 
36
44
  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:
@@ -38,3 +46,9 @@ From the repo root, non-interactive runs should load the SRAI MCP server and all
38
46
  `copilot -p "<your profiling instructions>" --additional-mcp-config '{"mcpServers":{"security-review-mcp":{"type":"stdio","command":"npx","args":["-y","@securityreviewai/security-review-mcp@latest"]}}}' --allow-all`
39
47
 
40
48
  During `securityreview-kit init`, choose **Yes** when asked to run GitHub Copilot CLI login, or pass **`--profiler-copilot-login`** with **`--profile-repo`** so `copilot login` and profiling stay in one run.
49
+
50
+ ## Codex CLI (scripted)
51
+
52
+ From the repo root, non-interactive runs should execute via `codex exec` and include the SRAI MCP server configuration for that run.
53
+
54
+ During `securityreview-kit init`, choose **Yes** when asked to run Codex login, or pass **`--profiler-codex-login`** with **`--profile-repo`** so `codex login --device-auth` and profiling stay in one run.
@@ -11,7 +11,7 @@ Configured SRAI project name: `<SRAI_PROJECT_NAME>`
11
11
 
12
12
  ## Canonical paths
13
13
 
14
- - **This skill & signal registry (read-only):** `<GUARDRAILS_SKILL_DIR>/` — e.g. `.cursor/skills/guardrails-profiler`, `.github/skills/guardrails-profiler`, `.claude/skills/guardrails-profiler`, or `.agents/skills/guardrails-profiler` for Codex depending on where this file was installed.
14
+ - **This skill & signal registry (read-only):** `<GUARDRAILS_SKILL_DIR>/` — e.g. `.cursor/skills/guardrails-profiler`, `.github/skills/guardrails-profiler`, `.claude/skills/guardrails-profiler`, or `.codex/skills/guardrails-profiler` for Codex depending on where this file was installed.
15
15
  - **Signal registry file:** `<GUARDRAILS_SKILL_DIR>/references/signal-registry.json`
16
16
  - **Local guardrails file:** `.guardrails/profile.json`
17
17
  - **Combined manifest (project root):** `profile.json` — includes guardrails profile, vibe profile fields for MCP, and default pack payload
@@ -38,7 +38,7 @@ export const TARGETS = {
38
38
  codex: {
39
39
  name: 'Codex',
40
40
  mcpConfigPath: '.codex/config.toml',
41
- rulePath: 'AGENTS.md',
41
+ rulePath: '.codex/AGENTS.md',
42
42
  ruleMode: 'append',
43
43
  detectDirs: ['.codex'],
44
44
  },
@@ -64,7 +64,7 @@ export const GUARDRAILS_PROFILER_SKILL_REL_DIR = {
64
64
  cursor: '.cursor/skills/guardrails-profiler',
65
65
  claude: '.claude/skills/guardrails-profiler',
66
66
  vscode: '.github/skills/guardrails-profiler',
67
- codex: '.agents/skills/guardrails-profiler',
67
+ codex: '.codex/skills/guardrails-profiler',
68
68
  };
69
69
 
70
70
  /** Relative workspace dirs for the guardrails-selection skill (per IDE / CLI). */
@@ -72,19 +72,21 @@ export const GUARDRAILS_SELECTION_SKILL_REL_DIR = {
72
72
  cursor: '.cursor/skills/guardrails-selection',
73
73
  claude: '.claude/skills/guardrails-selection',
74
74
  vscode: '.github/skills/guardrails-selection',
75
- codex: '.agents/skills/guardrails-selection',
75
+ codex: '.codex/skills/guardrails-selection',
76
76
  };
77
77
 
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
- codex: '.agents/skills/threat-modelling',
83
+ codex: '.codex/skills/threat-modelling',
83
84
  };
84
85
 
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
  };
@@ -1,8 +1,8 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { join } from 'node:path';
3
- import { GUARDRAILS_PROFILER_SKILL_REL_DIR, MCP_SERVER_NAME } from './constants.js';
3
+ import { GUARDRAILS_PROFILER_SKILL_REL_DIR, MCP_SERVER_NAME, MCP_SERVER_PACKAGE } from './constants.js';
4
4
  import { augmentPathEnv, resolveCursorAgentExecutable } from './cursor-agent-path.js';
5
- import { readJson } from './fs-helpers.js';
5
+ import { readJson, readText, writeText } from './fs-helpers.js';
6
6
 
7
7
  const PREFERRED_ORDER = ['cursor', 'vscode', 'claude', 'codex'];
8
8
 
@@ -11,6 +11,43 @@ function commandOk(cmd, args = ['--version'], env = process.env) {
11
11
  return r.status === 0;
12
12
  }
13
13
 
14
+ export function getProfilerLogPath(cwd, target) {
15
+ return join(cwd, '.guardrails', 'logs', `profiler-${target}.log`);
16
+ }
17
+
18
+ function buildProfilerEnv(target, streamProgress) {
19
+ const env = augmentPathEnv(process.env);
20
+ if (target === 'codex' && streamProgress && !env.RUST_LOG) {
21
+ env.RUST_LOG = 'info';
22
+ }
23
+ return env;
24
+ }
25
+
26
+ function persistProfilerLog(cwd, target, result) {
27
+ if (!result || (result.stdout == null && result.stderr == null)) {
28
+ return null;
29
+ }
30
+
31
+ const stdout = String(result.stdout || '');
32
+ const stderr = String(result.stderr || '');
33
+ const body = [
34
+ `target=${target}`,
35
+ `status=${result.status ?? ''}`,
36
+ `signal=${result.signal ?? ''}`,
37
+ '',
38
+ '--- stdout ---',
39
+ stdout,
40
+ '',
41
+ '--- stderr ---',
42
+ stderr,
43
+ '',
44
+ ].join('\n');
45
+
46
+ const logPath = getProfilerLogPath(cwd, target);
47
+ writeText(logPath, body);
48
+ return logPath;
49
+ }
50
+
14
51
  export function buildProfilerAgentPrompt(projectName, agentTarget = 'cursor') {
15
52
  const p = String(projectName || '').trim() || '<SRAI_PROJECT_NAME>';
16
53
  const skillRoot =
@@ -72,6 +109,75 @@ export function runCopilotLogin(cwd) {
72
109
  return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
73
110
  }
74
111
 
112
+ /**
113
+ * Run Claude Code login in the current terminal.
114
+ */
115
+ export function runClaudeLogin(cwd) {
116
+ const env = augmentPathEnv(process.env);
117
+ if (!commandOk('claude', ['--version'], env)) {
118
+ return {
119
+ ok: false,
120
+ status: null,
121
+ message: 'Claude Code CLI not found (`claude`). Install from https://claude.ai/code.',
122
+ };
123
+ }
124
+ const r = spawnSync('claude', ['auth', 'login', '--claudeai'], { cwd, stdio: 'inherit', env });
125
+ const spawnErr = r.error ? r.error.message : null;
126
+ if (r.status === null && spawnErr) {
127
+ return { ok: false, status: null, message: spawnErr };
128
+ }
129
+ return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
130
+ }
131
+
132
+ /**
133
+ * Run Codex login in the current terminal. Device auth keeps the flow in-terminal.
134
+ */
135
+ export function runCodexLogin(cwd) {
136
+ const env = augmentPathEnv(process.env);
137
+ if (!commandOk('codex', ['--version'], env)) {
138
+ return {
139
+ ok: false,
140
+ status: null,
141
+ message: 'Codex CLI not found (`codex`). Install with `npm install -g @openai/codex`.',
142
+ };
143
+ }
144
+ const r = spawnSync('codex', ['login', '--device-auth'], { cwd, stdio: 'inherit', env });
145
+ const spawnErr = r.error ? r.error.message : null;
146
+ if (r.status === null && spawnErr) {
147
+ return { ok: false, status: null, message: spawnErr };
148
+ }
149
+ return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
150
+ }
151
+
152
+ function escapeTomlString(value) {
153
+ return String(value || '').replaceAll('\\', '\\\\').replaceAll('"', '\\"');
154
+ }
155
+
156
+ function readCodexMcpEnv(cwd) {
157
+ const config = readText(join(cwd, '.codex', 'config.toml'));
158
+ const apiUrl = config.match(/^\s*SECURITY_REVIEW_API_URL\s*=\s*"([^"]*)"/m)?.[1] || '';
159
+ const apiToken = config.match(/^\s*SECURITY_REVIEW_API_TOKEN\s*=\s*"([^"]*)"/m)?.[1] || '';
160
+ return { apiUrl, apiToken };
161
+ }
162
+
163
+ export function buildCodexConfigOverrides(cwd, overrides = {}) {
164
+ const fileValues = readCodexMcpEnv(cwd);
165
+ const apiUrl = String(overrides.apiUrl || fileValues.apiUrl || '').trim();
166
+ const apiToken = String(overrides.apiToken || fileValues.apiToken || '').trim();
167
+
168
+ if (!apiUrl || !apiToken) {
169
+ return null;
170
+ }
171
+
172
+ return [
173
+ 'features.codex_hooks=true',
174
+ `mcp_servers.${MCP_SERVER_NAME}.command="npx"`,
175
+ `mcp_servers.${MCP_SERVER_NAME}.args=["-y","${MCP_SERVER_PACKAGE}@latest"]`,
176
+ `mcp_servers.${MCP_SERVER_NAME}.env.SECURITY_REVIEW_API_URL="${escapeTomlString(apiUrl)}"`,
177
+ `mcp_servers.${MCP_SERVER_NAME}.env.SECURITY_REVIEW_API_TOKEN="${escapeTomlString(apiToken)}"`,
178
+ ];
179
+ }
180
+
75
181
  export function pickProfilerAgentTarget(targets) {
76
182
  for (const t of PREFERRED_ORDER) {
77
183
  if (targets.includes(t)) {
@@ -94,10 +200,18 @@ export function pickProfilerAgentTarget(targets) {
94
200
  */
95
201
  export function runProfilerAgent(
96
202
  cwd,
97
- { target, projectName, cursorTrust = true, streamProgress = false, showOutput = streamProgress },
203
+ {
204
+ target,
205
+ projectName,
206
+ apiUrl,
207
+ apiToken,
208
+ cursorTrust = true,
209
+ streamProgress = false,
210
+ showOutput = streamProgress,
211
+ },
98
212
  ) {
99
213
  const prompt = buildProfilerAgentPrompt(projectName, target);
100
- const env = augmentPathEnv(process.env);
214
+ const env = buildProfilerEnv(target, streamProgress);
101
215
  const opts = showOutput
102
216
  ? { cwd, stdio: 'inherit', env }
103
217
  : { cwd, stdio: ['ignore', 'pipe', 'pipe'], env, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 };
@@ -129,27 +243,45 @@ export function runProfilerAgent(
129
243
  if (r.error) {
130
244
  return { ok: false, message: r.error.message };
131
245
  }
132
- return buildProfilerResult(r);
246
+ return buildProfilerResult(r, { logPath: showOutput ? null : persistProfilerLog(cwd, target, r) });
133
247
  }
134
248
 
135
249
  if (target === 'claude') {
136
250
  if (!commandOk('claude', ['--version'], env)) {
137
251
  return { ok: false, message: 'claude not on PATH' };
138
252
  }
139
- const args = streamProgress
140
- ? ['-p', '--output-format', 'stream-json', '--include-partial-messages', '--verbose', prompt]
141
- : ['-p', prompt];
253
+ const settingsPath = join(cwd, '.claude', 'settings.json');
254
+ const args = ['-p', '--settings', settingsPath, '--model', 'haiku'];
255
+ if (streamProgress) {
256
+ args.push('--output-format', 'stream-json', '--include-partial-messages', '--include-hook-events', '--verbose');
257
+ }
258
+ args.push(prompt);
142
259
  const r = spawnSync('claude', args, opts);
143
- return buildProfilerResult(r);
260
+ return buildProfilerResult(r, { logPath: showOutput ? null : persistProfilerLog(cwd, target, r) });
144
261
  }
145
262
 
146
263
  if (target === 'codex') {
147
264
  if (!commandOk('codex', ['--version'], env)) {
148
265
  return { ok: false, message: 'codex not on PATH' };
149
266
  }
150
- const args = streamProgress ? ['exec', '--json', prompt] : ['exec', prompt];
267
+ const configOverrides = buildCodexConfigOverrides(cwd, { apiUrl, apiToken });
268
+ if (!configOverrides) {
269
+ return {
270
+ ok: false,
271
+ message:
272
+ 'Codex profiling needs SRAI MCP credentials. Re-run init with Codex selected and MCP installation enabled.',
273
+ };
274
+ }
275
+ const args = ['exec', '--full-auto'];
276
+ for (const override of configOverrides) {
277
+ args.push('-c', override);
278
+ }
279
+ if (streamProgress) {
280
+ args.push('--json');
281
+ }
282
+ args.push(prompt);
151
283
  const r = spawnSync('codex', args, opts);
152
- return buildProfilerResult(r);
284
+ return buildProfilerResult(r, { logPath: showOutput ? null : persistProfilerLog(cwd, target, r) });
153
285
  }
154
286
 
155
287
  if (target === 'vscode') {
@@ -177,7 +309,7 @@ export function runProfilerAgent(
177
309
  args.push('--output-format', 'json');
178
310
  }
179
311
  const r = spawnSync('copilot', args, opts);
180
- return buildProfilerResult(r);
312
+ return buildProfilerResult(r, { logPath: showOutput ? null : persistProfilerLog(cwd, target, r) });
181
313
  }
182
314
 
183
315
  return { ok: false, message: 'unsupported agent target' };
@@ -207,13 +339,13 @@ export function buildCopilotAdditionalMcpConfig(cwd) {
207
339
  });
208
340
  }
209
341
 
210
- function buildProfilerResult(result) {
342
+ function buildProfilerResult(result, extras = {}) {
211
343
  if (result.error) {
212
- return { ok: false, status: result.status, message: result.error.message };
344
+ return { ok: false, status: result.status, message: result.error.message, ...extras };
213
345
  }
214
346
 
215
347
  if (result.status === 0) {
216
- return { ok: true, status: result.status };
348
+ return { ok: true, status: result.status, ...extras };
217
349
  }
218
350
 
219
351
  const outputTail = summarizeProfilerOutput(result.stderr || result.stdout || '');
@@ -228,6 +360,7 @@ function buildProfilerResult(result) {
228
360
  ok: false,
229
361
  status: result.status,
230
362
  message: outputTail ? `${exitDetail}; last output: ${outputTail}` : exitDetail,
363
+ ...extras,
231
364
  };
232
365
  }
233
366
 
@@ -4,6 +4,7 @@ import { join } from 'node:path';
4
4
  import { test } from 'node:test';
5
5
  import assert from 'node:assert/strict';
6
6
  import {
7
+ buildCodexConfigOverrides,
7
8
  buildCopilotAdditionalMcpConfig,
8
9
  pickProfilerAgentTarget,
9
10
  } from './profiler-agent.js';
@@ -39,3 +40,34 @@ test('buildCopilotAdditionalMcpConfig converts VS Code MCP config', () => {
39
40
  assert.equal(converted.mcpServers['security-review-mcp'].command, 'npx');
40
41
  assert.deepEqual(converted.mcpServers['security-review-mcp'].tools, ['*']);
41
42
  });
43
+
44
+ test('buildCodexConfigOverrides builds inline MCP config for profiling', () => {
45
+ const cwd = join(tmpdir(), `securityreview-kit-codex-${Date.now()}`);
46
+ mkdirSync(join(cwd, '.codex'), { recursive: true });
47
+ writeFileSync(
48
+ join(cwd, '.codex', 'config.toml'),
49
+ [
50
+ '[features]',
51
+ 'codex_hooks = true',
52
+ '',
53
+ '[mcp_servers.security-review-mcp]',
54
+ 'command = "npx"',
55
+ 'args = ["-y", "@securityreviewai/security-review-mcp@latest"]',
56
+ '',
57
+ '[mcp_servers.security-review-mcp.env]',
58
+ 'SECURITY_REVIEW_API_URL = "https://api.example.test"',
59
+ 'SECURITY_REVIEW_API_TOKEN = "token"',
60
+ '',
61
+ ].join('\n'),
62
+ );
63
+
64
+ const overrides = buildCodexConfigOverrides(cwd);
65
+
66
+ assert.deepEqual(overrides, [
67
+ 'features.codex_hooks=true',
68
+ 'mcp_servers.security-review-mcp.command="npx"',
69
+ 'mcp_servers.security-review-mcp.args=["-y","@securityreviewai/security-review-mcp@latest"]',
70
+ 'mcp_servers.security-review-mcp.env.SECURITY_REVIEW_API_URL="https://api.example.test"',
71
+ 'mcp_servers.security-review-mcp.env.SECURITY_REVIEW_API_TOKEN="token"',
72
+ ]);
73
+ });