@node9/proxy 1.58.5 → 1.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +47 -0
  2. package/dist/cli.js +655 -67
  3. package/dist/cli.mjs +655 -67
  4. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -52974,9 +52974,39 @@ var SURFACE_FILES = [
52974
52974
  ".claude/settings.local.json",
52975
52975
  ".mcp.json",
52976
52976
  ".cursor/mcp.json",
52977
- ".codex/config.toml"
52977
+ ".codex/config.toml",
52978
+ // CI-6: agent instruction files (auto-loaded into the agent's system prompt).
52979
+ "CLAUDE.md",
52980
+ "AGENTS.md",
52981
+ "GEMINI.md",
52982
+ ".cursorrules",
52983
+ ".github/copilot-instructions.md",
52984
+ ".windsurfrules",
52985
+ ".clinerules"
52978
52986
  ];
52979
52987
  var WORKFLOW_DIR = ".github/workflows";
52988
+ var SURFACE_BASENAME = /(^|\/)(CLAUDE|AGENTS|GEMINI)\.md$|(^|\/)\.cursorrules$|(^|\/)\.(windsurf|cline)rules$|(^|\/)copilot-instructions\.md$|(^|\/)\.claude\/settings(\.local)?\.json$|(^|\/)\.mcp\.json$|(^|\/)\.cursor\/mcp\.json$|(^|\/)\.codex\/config\.toml$/;
52989
+ var IGNORE_HARD = /(^|\/)(node_modules|vendor|\.git|\.next|\.venv|site-packages)\//;
52990
+ var IGNORE_SOFT = /(^|\/)(dist|build|out|target)\//;
52991
+ var isIgnoredDir = (relSlash) => IGNORE_HARD.test(relSlash) || IGNORE_SOFT.test(relSlash);
52992
+ var MAX_SURFACE_FILES = 200;
52993
+ function pickSurfacePaths(paths, truncated, notes) {
52994
+ const surface = paths.filter((p) => SURFACE_BASENAME.test(p) && !IGNORE_HARD.test(p));
52995
+ const matched = surface.filter((p) => !IGNORE_SOFT.test(p));
52996
+ const softSkipped = surface.filter((p) => IGNORE_SOFT.test(p));
52997
+ const capped = matched.slice(0, MAX_SURFACE_FILES);
52998
+ if (truncated || matched.length > MAX_SURFACE_FILES) {
52999
+ notes.push(
53000
+ `repo tree is large/truncated \u2014 some agent-surface files may be INCOMPLETE (scanned ${capped.length} of ${matched.length}${truncated ? "+" : ""}).`
53001
+ );
53002
+ }
53003
+ if (softSkipped.length) {
53004
+ notes.push(
53005
+ `skipped ${softSkipped.length} agent-surface file(s) under a build-output dir (dist/build/out/target), e.g. ${softSkipped.slice(0, 3).join(", ")} \u2014 if any is a real committed config, move it out of the build dir to have it scanned.`
53006
+ );
53007
+ }
53008
+ return capped;
53009
+ }
52980
53010
  async function pooled(items, limit, fn) {
52981
53011
  const out = new Array(items.length);
52982
53012
  let next = 0;
@@ -53068,13 +53098,37 @@ async function listWorkflowPaths(owner, repo, notes) {
53068
53098
  if (status !== 200 || !Array.isArray(json)) return [];
53069
53099
  return json.filter((e) => e.type === "file" && /\.ya?ml$/.test(e.name)).map((e) => e.path);
53070
53100
  }
53101
+ var ROOT_WORKFLOW_RE = /^\.github\/workflows\/[^/]+\.ya?ml$/;
53102
+ async function listSurfaceTree(owner, repo, notes) {
53103
+ const url = `https://api.github.com/repos/${owner}/${repo}/git/trees/HEAD?recursive=1`;
53104
+ const { status, json } = await ghGet(url);
53105
+ if (status === 403 || status === 429) {
53106
+ if (!notes.includes(RATE_LIMIT_NOTE)) notes.push(RATE_LIMIT_NOTE);
53107
+ return null;
53108
+ }
53109
+ if (status === 0) {
53110
+ if (!notes.includes(NETWORK_NOTE)) notes.push(NETWORK_NOTE);
53111
+ return null;
53112
+ }
53113
+ if (status !== 200 || !json || typeof json !== "object") return null;
53114
+ const tree = json;
53115
+ if (!Array.isArray(tree.tree)) return null;
53116
+ const blobs = tree.tree.filter((e) => e.type === "blob" && typeof e.path === "string").map((e) => e.path);
53117
+ return {
53118
+ surface: pickSurfacePaths(blobs, !!tree.truncated, notes),
53119
+ workflows: blobs.filter((p) => ROOT_WORKFLOW_RE.test(p))
53120
+ };
53121
+ }
53071
53122
  var FETCH_CONCURRENCY = 8;
53072
53123
  async function fetchGitHubTree(owner, repo, onProgress) {
53073
53124
  const notes = [];
53074
53125
  try {
53075
- onProgress?.({ phase: "listing workflows", done: 0, total: 1 });
53076
- const workflowPaths = await listWorkflowPaths(owner, repo, notes);
53077
- const allPaths = [...SURFACE_FILES, ...workflowPaths];
53126
+ onProgress?.({ phase: "discovering agent surface", done: 0, total: 1 });
53127
+ const discovered = await listSurfaceTree(owner, repo, notes);
53128
+ const workflowPaths = discovered ? discovered.workflows : await listWorkflowPaths(owner, repo, notes);
53129
+ const allPaths = [
53130
+ .../* @__PURE__ */ new Set([...SURFACE_FILES, ...discovered?.surface ?? [], ...workflowPaths])
53131
+ ];
53078
53132
  let done = 0;
53079
53133
  const fetched = await pooled(allPaths, FETCH_CONCURRENCY, async (p) => {
53080
53134
  const f = await fetchOne(owner, repo, p, notes);
@@ -53083,7 +53137,9 @@ async function fetchGitHubTree(owner, repo, onProgress) {
53083
53137
  });
53084
53138
  return { source: `${owner}/${repo}`, files: fetched.filter((f) => !!f), notes };
53085
53139
  } catch (err2) {
53086
- notes.push(`fetch degraded: ${err2?.message ?? "network error"}`);
53140
+ notes.push(
53141
+ `fetch degraded: ${err2?.message ?? "network error"} \u2014 results may be INCOMPLETE (the repo could not be fetched).`
53142
+ );
53087
53143
  return { source: `${owner}/${repo}`, files: [], notes };
53088
53144
  }
53089
53145
  }
@@ -53100,7 +53156,42 @@ function readLocalTree(dir) {
53100
53156
  } catch {
53101
53157
  }
53102
53158
  };
53103
- for (const p of SURFACE_FILES) add(p);
53159
+ const seen = /* @__PURE__ */ new Set();
53160
+ const collect = (rel) => {
53161
+ if (seen.has(rel)) return;
53162
+ seen.add(rel);
53163
+ add(rel);
53164
+ };
53165
+ for (const p of SURFACE_FILES) collect(p);
53166
+ const matches = [];
53167
+ const MAX_DIRS = 5e3;
53168
+ let dirsVisited = 0;
53169
+ const walk = (relDir) => {
53170
+ if (matches.length >= MAX_SURFACE_FILES || dirsVisited >= MAX_DIRS) return;
53171
+ dirsVisited++;
53172
+ let entries;
53173
+ try {
53174
+ entries = import_fs59.default.readdirSync(import_path56.default.join(root, relDir), { withFileTypes: true });
53175
+ } catch {
53176
+ return;
53177
+ }
53178
+ for (const e of entries) {
53179
+ if (matches.length >= MAX_SURFACE_FILES || dirsVisited >= MAX_DIRS) return;
53180
+ const rel = relDir ? `${relDir}/${e.name}` : e.name;
53181
+ if (e.isDirectory()) {
53182
+ if (isIgnoredDir(`${rel}/`)) continue;
53183
+ walk(rel);
53184
+ } else if (e.isFile() && SURFACE_BASENAME.test(rel)) {
53185
+ matches.push(rel);
53186
+ }
53187
+ }
53188
+ };
53189
+ walk("");
53190
+ if (matches.length >= MAX_SURFACE_FILES || dirsVisited >= MAX_DIRS)
53191
+ notes.push(
53192
+ `repo is large \u2014 some agent-surface files may be INCOMPLETE (capped at ${MAX_SURFACE_FILES} files / ${MAX_DIRS} dirs).`
53193
+ );
53194
+ for (const rel of matches) collect(rel);
53104
53195
  const wfDir = import_path56.default.join(root, WORKFLOW_DIR);
53105
53196
  try {
53106
53197
  if (import_fs59.default.existsSync(wfDir)) {
@@ -53141,6 +53232,18 @@ var AGENT_ACTION_RE = /(anthropics\/claude-code(-base)?-action|anthropics\/claud
53141
53232
  var BROAD_TOOL_RE = /(^|["\s,])(Bash|Write|Edit)(\s|,|"|$)|Bash\(\s*\*|Bash\((curl|wget|git push|git commit|git config|git:|rm|sh|bash|eval|npx|pip)/i;
53142
53233
  var EXFIL_RCE_RE = /(^|["\s,])Bash(\s|,|"|$)|Bash\(\s*\*|Bash\((curl|wget|sh[\s):]|eval|rm[\s):]|git push)/i;
53143
53234
  var GH_WRITE_TOOL_RE = /Bash\(\s*(gh api|gh:|gh (pr|issue) (comment|edit|merge|close|review|create|ready|lock|reopen)|git push|git:)/i;
53235
+ var EGRESS_TOOL_RE = /(^|["\s,])Bash(\s|,|"|$)|Bash\(\s*\*|Bash\(\s*(curl|wget|python|python3|node|deno|bun|ruby|perl|php|pip|pipx|npx|nc|ncat|ssh|scp|http|gh api)|WebFetch|WebSearch/i;
53236
+ var MEMBERSHIP_CHECK_RE = /orgs\/[^/'"\s]+\/memberships\/|teams\/[^/'"\s]+\/memberships\/|getCollaboratorPermissionLevel|collaborators\/[^/'"\s]+\/permission|checkMembershipForUser|getMembershipForUser/i;
53237
+ var STRANGER_ISSUE_TYPES = /* @__PURE__ */ new Set(["opened", "edited", "reopened", "closed"]);
53238
+ var STRANGER_PR_TYPES = /* @__PURE__ */ new Set([
53239
+ "opened",
53240
+ "edited",
53241
+ "reopened",
53242
+ "closed",
53243
+ "synchronize",
53244
+ "ready_for_review",
53245
+ "converted_to_draft"
53246
+ ]);
53144
53247
  function str(v) {
53145
53248
  return typeof v === "string" ? v : v == null ? "" : JSON.stringify(v);
53146
53249
  }
@@ -53151,9 +53254,57 @@ function triggerKeys(wf, raw) {
53151
53254
  if (on && typeof on === "object") return Object.keys(on);
53152
53255
  return [];
53153
53256
  }
53257
+ function onObject(wf, raw) {
53258
+ return wf.on ?? raw["on"] ?? raw[true];
53259
+ }
53260
+ function activityTypes(node) {
53261
+ return node && Array.isArray(node.types) ? node.types.map(String) : [];
53262
+ }
53263
+ function keyStrangerFirable(on, key) {
53264
+ switch (key) {
53265
+ case "issue_comment":
53266
+ case "pull_request_review":
53267
+ case "pull_request_review_comment":
53268
+ case "workflow_run":
53269
+ case "discussion":
53270
+ case "discussion_comment":
53271
+ case "fork":
53272
+ case "watch":
53273
+ case "public":
53274
+ return true;
53275
+ case "pull_request":
53276
+ case "pull_request_target": {
53277
+ const ts = activityTypes(on[key]);
53278
+ return ts.length ? ts.some((t) => STRANGER_PR_TYPES.has(t)) : true;
53279
+ }
53280
+ case "issues": {
53281
+ const ts = activityTypes(on["issues"]);
53282
+ return ts.length ? ts.some((t) => STRANGER_ISSUE_TYPES.has(t)) : true;
53283
+ }
53284
+ // workflow_call / workflow_dispatch / schedule / push / create / delete / …
53285
+ // are NOT stranger-firable (require write access, or run in a trusted context).
53286
+ default:
53287
+ return false;
53288
+ }
53289
+ }
53290
+ function triggerReach(wf, raw) {
53291
+ const on = onObject(wf, raw) ?? {};
53292
+ const keys = triggerKeys(wf, raw);
53293
+ const firable = keys.filter((k) => keyStrangerFirable(on, k));
53294
+ const untrusted = firable.length > 0;
53295
+ const reusable = !untrusted && keys.some((k) => /^workflow_call$/i.test(k));
53296
+ const secretExposed = firable.some((t) => /pull_request_target|workflow_run/i.test(t));
53297
+ const privileged = untrusted && firable.some(
53298
+ (t) => /pull_request_target|pull_request_review|workflow_run|issue|discussion/i.test(t)
53299
+ );
53300
+ return { keys, untrusted, reusable, secretExposed, privileged };
53301
+ }
53302
+ function jobList(wf) {
53303
+ return Object.values(wf.jobs ?? {}).filter((j) => j != null);
53304
+ }
53154
53305
  function allSteps(wf) {
53155
53306
  const out = [];
53156
- for (const job of Object.values(wf.jobs ?? {})) {
53307
+ for (const job of jobList(wf)) {
53157
53308
  for (const step of job.steps ?? []) out.push({ job, step });
53158
53309
  }
53159
53310
  return out;
@@ -53164,18 +53315,32 @@ function isAgentStep(step) {
53164
53315
  return true;
53165
53316
  return false;
53166
53317
  }
53318
+ function allowedToolsFromArgs(claudeArgs) {
53319
+ let out = "";
53320
+ for (const m of claudeArgs.matchAll(/--allowed[-_]?tools[=\s]+("[^"]*"|'[^']*'|\S+)/gi))
53321
+ out += " " + m[1];
53322
+ return out;
53323
+ }
53324
+ function allowedToolsFromSettings(settings) {
53325
+ let out = "";
53326
+ for (const key of ["allowedTools", "allow"]) {
53327
+ for (const m of settings.matchAll(new RegExp(`"${key}"\\s*:\\s*(\\[[^\\]]*\\])`, "gi")))
53328
+ out += " " + m[1];
53329
+ }
53330
+ return out;
53331
+ }
53167
53332
  function collectTools(steps) {
53168
- const stripDeny = (x) => x.replace(/--disallowed[-_]?tools\s+("[^"]*"|'[^']*'|\S+)/gi, " ").replace(/["']disallowed[_]?[tT]ools["']\s*:\s*\[[^\]]*\]/g, " ");
53169
53333
  let s = "";
53170
53334
  for (const st of steps) {
53171
53335
  const w = st.with ?? {};
53172
- s += " " + stripDeny(str(w["claude_args"])) + " " + str(w["allowed_tools"]) + " " + str(w["allowedTools"]);
53173
- s += " " + stripDeny(str(w["settings"]));
53336
+ s += " " + allowedToolsFromArgs(str(w["claude_args"]));
53337
+ s += " " + str(w["allowed_tools"]) + " " + str(w["allowedTools"]);
53338
+ s += " " + allowedToolsFromSettings(str(w["settings"]));
53174
53339
  }
53175
53340
  return s;
53176
53341
  }
53177
- function untrustedHeadCheckout(wf) {
53178
- for (const { step } of allSteps(wf)) {
53342
+ function untrustedHeadCheckout(steps) {
53343
+ for (const step of steps) {
53179
53344
  if (!step.uses || !/actions\/checkout/.test(step.uses)) continue;
53180
53345
  const ref = str(step.with?.["ref"]);
53181
53346
  if (/pull_request\.head|head[._]sha|head_ref|expected_head|workflow_run\.head|inputs\.[\w]*head/i.test(
@@ -53194,43 +53359,100 @@ function promptTakesUntrusted(steps) {
53194
53359
  }
53195
53360
  return false;
53196
53361
  }
53197
- function hasActorGate(wf, raw) {
53362
+ var NONCONTAINS_GATE_RE = new RegExp(
53363
+ [
53364
+ String.raw`==\s*['"]?(OWNER|MEMBER|COLLABORATOR)\b`,
53365
+ String.raw`==\s*['"](write|admin|maintain)`,
53366
+ String.raw`github\.actor\s*==`,
53367
+ String.raw`user\.login\s*==`,
53368
+ String.raw`head\.repo\.full_name\s*==\s*github\.repository`
53369
+ ].join("|"),
53370
+ "i"
53371
+ );
53372
+ var PERMISSION_OUTPUT_GATE_RE = /steps\.[\w-]+\.outputs\.[\w-]*(permission|allowed|authoriz|is[_-]?(admin|member|maintainer|collaborator))[\w-]*\s*==\s*['"]?(true|admin|write|maintain)/i;
53373
+ var CONTAINS_GATE_RE = /contains\((?:[^()]|\([^()]*\))*(login|actor|association|OWNER|MEMBER|COLLABORATOR)/i;
53374
+ var NEGATED_CONTAINS_RE = /!\s*\(?\s*contains\((?:[^()]|\([^()]*\))*(login|actor|association|OWNER|MEMBER|COLLABORATOR)/i;
53375
+ function labelTypeConfigured(wf, raw) {
53198
53376
  const on = wf.on ?? raw["on"] ?? raw[true];
53199
53377
  const prTypes = (t) => t && Array.isArray(t.types) ? t.types.map(String) : [];
53200
- const labeled = prTypes(on?.["pull_request_target"]).includes("labeled") || prTypes(on?.["pull_request"]).includes("labeled");
53201
- const ifs = [
53202
- wf.jobs ? Object.values(wf.jobs).map((j) => j.if) : [],
53203
- allSteps(wf).map((s) => s.step.if)
53204
- ].flat().map(str).join(" ");
53205
- const gated = /author_association|FIRST_TIME_CONTRIBUTOR|collaborator|\b(MEMBER|OWNER)\b|permission|github\.actor\s*==|user\.login\s*==|==\s*['"](write|admin|maintain)|contains\([^)]*(login|actor|association)|head\.repo\.full_name\s*==\s*github\.repository/i.test(
53206
- ifs
53207
- );
53208
- const labelGated = !!labeled && /event\.label|label\.name/i.test(ifs);
53378
+ return prTypes(on?.["pull_request_target"]).includes("labeled") || prTypes(on?.["pull_request"]).includes("labeled");
53379
+ }
53380
+ function ifsAreGated(ifs, labelConfigured) {
53381
+ const containsGate = CONTAINS_GATE_RE.test(ifs) && !NEGATED_CONTAINS_RE.test(ifs);
53382
+ const gated = NONCONTAINS_GATE_RE.test(ifs) || containsGate;
53383
+ const labelGated = labelConfigured && /event\.label|label\.name/i.test(ifs);
53209
53384
  return gated || labelGated;
53210
53385
  }
53386
+ function hasActorGate(wf, raw) {
53387
+ const ifs = [jobList(wf).map((j) => j.if), allSteps(wf).map((s) => s.step.if)].flat().map(str).join(" ");
53388
+ return ifsAreGated(ifs, labelTypeConfigured(wf, raw));
53389
+ }
53390
+ function hasStepMembershipGate(job) {
53391
+ const steps = job.steps ?? [];
53392
+ const gateIds = steps.filter((s) => s.id && MEMBERSHIP_CHECK_RE.test(str(s.run))).map((s) => s.id);
53393
+ if (!gateIds.length) return false;
53394
+ return steps.some(
53395
+ (s) => isAgentStep(s) && gateIds.some(
53396
+ (id) => new RegExp(`steps\\.${id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.outputs\\.`).test(
53397
+ str(s.if)
53398
+ )
53399
+ )
53400
+ );
53401
+ }
53402
+ function jobActorGate(job, wf, raw) {
53403
+ const ifs = [job.if, ...(job.steps ?? []).map((s) => s.if)].map(str).join(" ");
53404
+ return ifsAreGated(ifs, labelTypeConfigured(wf, raw)) || /assignee\.login\s*==|event\.assignee\b/i.test(ifs) || PERMISSION_OUTPUT_GATE_RE.test(ifs) || // [2] job-scoped permission-check-output gate
53405
+ hasStepMembershipGate(job);
53406
+ }
53211
53407
  function hasImplicitActorGate(agentSteps, bypassActive) {
53212
53408
  if (bypassActive) return false;
53213
53409
  return agentSteps.some((s) => /anthropics\/claude-code-action@/i.test(s.uses ?? ""));
53214
53410
  }
53215
- function permsElevated(wf, agentJobs) {
53216
- const check = (p) => {
53217
- const s = str(p);
53218
- return /["']?(contents|id-token|packages)["']?\s*:\s*["']?write/i.test(s);
53219
- };
53220
- return check(wf.permissions) || agentJobs.some((j) => check(j.permissions));
53411
+ function injectableJobs(wf, raw, untrustedTrigger) {
53412
+ const out = [];
53413
+ for (const job of jobList(wf)) {
53414
+ const a = (job.steps ?? []).filter(isAgentStep);
53415
+ if (!a.length) continue;
53416
+ const jobStar = str(a.map((s) => s.with?.["allowed_non_write_users"]).find(Boolean)) === "*";
53417
+ if (jobActorGate(job, wf, raw) || hasImplicitActorGate(a, jobStar && untrustedTrigger))
53418
+ continue;
53419
+ out.push(job);
53420
+ }
53421
+ return out;
53422
+ }
53423
+ function usesPatIn(jobs) {
53424
+ for (const j of jobs)
53425
+ for (const step of j.steps ?? []) {
53426
+ const gt = str(step.with?.["github_token"]);
53427
+ if (/secrets\./i.test(gt) && !/secrets\.GITHUB_TOKEN/i.test(gt)) return true;
53428
+ }
53429
+ return false;
53430
+ }
53431
+ function jobPerms(wf, job) {
53432
+ return job.permissions != null ? str(job.permissions) : str(wf.permissions);
53433
+ }
53434
+ function jobsHaveWrite(wf, jobs, rx) {
53435
+ return jobs.some((j) => {
53436
+ const p = jobPerms(wf, j);
53437
+ return /\bwrite-all\b/i.test(p) || rx.test(p);
53438
+ });
53439
+ }
53440
+ function hasIdTokenWrite(wf, jobs) {
53441
+ const rx = /["']?id-token["']?\s*:\s*["']?write/i;
53442
+ return jobs.some((j) => rx.test(jobPerms(wf, j)));
53443
+ }
53444
+ function hasPrWritePerm(wf, jobs) {
53445
+ return jobsHaveWrite(wf, jobs, /["']?pull-requests["']?\s*:\s*["']?write/i);
53221
53446
  }
53222
- function hasGithubWritePerm(wf, agentJobs) {
53223
- const check = (p) => /["']?(contents|pull-requests|issues|packages|actions|deployments)["']?\s*:\s*["']?write/i.test(
53224
- str(p)
53447
+ function hasCodeWritePerm(wf, agentJobs) {
53448
+ return jobsHaveWrite(
53449
+ wf,
53450
+ agentJobs,
53451
+ /["']?(contents|packages|actions|deployments)["']?\s*:\s*["']?write/i
53225
53452
  );
53226
- return check(wf.permissions) || agentJobs.some((j) => check(j.permissions));
53227
53453
  }
53228
- function usesPat(wf) {
53229
- for (const { step } of allSteps(wf)) {
53230
- const gt = str(step.with?.["github_token"]);
53231
- if (/secrets\./i.test(gt) && !/secrets\.GITHUB_TOKEN/i.test(gt)) return true;
53232
- }
53233
- return false;
53454
+ function hasMetaWritePerm(wf, agentJobs) {
53455
+ return jobsHaveWrite(wf, agentJobs, /["']?(pull-requests|issues)["']?\s*:\s*["']?write/i);
53234
53456
  }
53235
53457
  function hasEnvDeny(steps) {
53236
53458
  return steps.some((s) => /"?mode"?\s*:\s*"?deny/i.test(str(s.with?.["settings"])));
@@ -53263,31 +53485,38 @@ function analyzeWorkflow(path70, content) {
53263
53485
  )
53264
53486
  ];
53265
53487
  if (agentSteps.length === 0) return null;
53266
- const triggers = triggerKeys(wf, raw);
53267
- const secretExposed = triggers.some((t) => /pull_request_target|workflow_run/i.test(t));
53268
- const forkInput = triggers.some((t) => /issue|pull_request|workflow_call/i.test(t));
53269
- const privileged = triggers.some(
53270
- (t) => /pull_request_target|pull_request_review|workflow_run|workflow_call|issue|discussion/i.test(t)
53271
- );
53488
+ const {
53489
+ keys: triggers,
53490
+ untrusted: forkInput,
53491
+ secretExposed,
53492
+ reusable,
53493
+ privileged
53494
+ } = triggerReach(wf, raw);
53272
53495
  const nonWrite = str(agentSteps.map((s) => s.with?.["allowed_non_write_users"]).find(Boolean));
53273
53496
  const nonWriteStar = nonWrite === "*";
53274
53497
  const nonWriteList = !!nonWrite && nonWrite !== "*";
53275
- const untrustedTrigger = secretExposed || forkInput;
53498
+ const untrustedTrigger = forkInput || reusable;
53276
53499
  const bypassActive = nonWriteStar && untrustedTrigger;
53277
- const head = untrustedHeadCheckout(wf);
53500
+ const head = untrustedHeadCheckout(steps);
53278
53501
  const promptUntrusted = promptTakesUntrusted(agentSteps);
53279
- const reach = Math.max(
53502
+ const injJobs = injectableJobs(wf, raw, untrustedTrigger);
53503
+ const powerJobs = injJobs.length ? injJobs : agentJobs;
53504
+ const powerSteps = injJobs.flatMap((j) => (j.steps ?? []).filter(isAgentStep));
53505
+ const scopedSteps = powerSteps.length ? powerSteps : agentSteps;
53506
+ const toolsBlob = collectTools(scopedSteps);
53507
+ const reach = untrustedTrigger && injJobs.length ? Math.max(
53280
53508
  head === "root" ? 3 : head === "subdir" ? 1 : 0,
53281
53509
  promptUntrusted ? 2 : 0,
53282
53510
  bypassActive ? 2 : 0
53283
- );
53284
- const toolsBlob = collectTools(agentSteps);
53511
+ ) : 0;
53285
53512
  const broadTools = BROAD_TOOL_RE.test(toolsBlob);
53286
- const elevated = permsElevated(wf, agentJobs);
53287
- const pat = usesPat(wf);
53513
+ const egressTool = EGRESS_TOOL_RE.test(toolsBlob);
53514
+ const elevated = hasCodeWritePerm(wf, powerJobs) || hasIdTokenWrite(wf, powerJobs) && egressTool;
53515
+ const pat = usesPatIn(powerJobs);
53288
53516
  const power = (broadTools ? 2 : 0) + (bypassActive ? 1 : 0) + (elevated ? 1 : 0) + (pat ? 1 : 0);
53289
53517
  const explicitGate = hasActorGate(wf, raw);
53290
53518
  const implicitGate = hasImplicitActorGate(agentSteps, bypassActive);
53519
+ const membershipGated = injJobs.length === 0 && jobList(wf).some((j) => (j.steps ?? []).some(isAgentStep) && hasStepMembershipGate(j));
53291
53520
  const gate = explicitGate || implicitGate;
53292
53521
  const envDeny = hasEnvDeny(agentSteps);
53293
53522
  const pinned = agentActionsPinned(agentSteps);
@@ -53296,7 +53525,7 @@ function analyzeWorkflow(path70, content) {
53296
53525
  if (envDeny) score -= 1;
53297
53526
  if (pinned) score -= 1;
53298
53527
  score = Math.max(0, score);
53299
- if (score === 0 && !secretExposed) return null;
53528
+ if (score === 0 && !secretExposed && !reusable) return null;
53300
53529
  let severity = severityFromScore(score);
53301
53530
  if (gate && severity && severity !== "advisory") severity = "advisory";
53302
53531
  if (reach === 0 && severity && severity !== "advisory") severity = "advisory";
@@ -53304,9 +53533,20 @@ function analyzeWorkflow(path70, content) {
53304
53533
  severity = "medium";
53305
53534
  const exfilOrRce = EXFIL_RCE_RE.test(toolsBlob);
53306
53535
  const githubWriteTool = GH_WRITE_TOOL_RE.test(toolsBlob);
53307
- const canDamage = exfilOrRce || (hasGithubWritePerm(wf, agentJobs) || pat) && githubWriteTool;
53536
+ const rce = exfilOrRce;
53537
+ const codeWrite = pat || hasCodeWritePerm(wf, powerJobs) && githubWriteTool;
53538
+ const metaWrite = hasMetaWritePerm(wf, powerJobs) && githubWriteTool;
53539
+ const canDamage = rce || codeWrite || metaWrite;
53308
53540
  if (!canDamage && severity && SEVERITY_RANK2[severity] > SEVERITY_RANK2.medium) severity = "medium";
53309
- if (untrustedTrigger && !privileged && severity && severity !== "advisory") severity = "advisory";
53541
+ if (canDamage && !rce && !codeWrite && severity === "critical") severity = "high";
53542
+ const issueOnlyWrite = metaWrite && !hasPrWritePerm(wf, powerJobs) && !hasCodeWritePerm(wf, powerJobs);
53543
+ if (issueOnlyWrite && !rce && severity && SEVERITY_RANK2[severity] > SEVERITY_RANK2.medium)
53544
+ severity = "medium";
53545
+ if (untrustedTrigger && !privileged && !reusable && severity && severity !== "advisory")
53546
+ severity = "advisory";
53547
+ const reusableLoadedGun = reusable && (head === "root" || promptUntrusted);
53548
+ if (reusable && !reusableLoadedGun && severity && SEVERITY_RANK2[severity] > SEVERITY_RANK2.medium)
53549
+ severity = "medium";
53310
53550
  if (!severity) severity = "advisory";
53311
53551
  const signals = [];
53312
53552
  if (secretExposed)
@@ -53314,7 +53554,11 @@ function analyzeWorkflow(path70, content) {
53314
53554
  `runs with base-repo secrets (${triggers.filter((t) => /target|workflow_run/i.test(t)).join(", ")})`
53315
53555
  );
53316
53556
  else if (forkInput) signals.push(`triggered by untrusted input (${triggers.join(", ")})`);
53317
- if (untrustedTrigger && !privileged)
53557
+ else if (reusable)
53558
+ signals.push(
53559
+ reusableLoadedGun ? `reusable workflow (${triggers.join(", ")}) that checks out an untrusted head / ingests untrusted input \u2014 exploitable the moment a caller wires a fork trigger (reachability depends on the caller, but this workflow is built to process attacker input)` : `reusable workflow (${triggers.join(", ")}) \u2014 no untrusted trigger of its own; reachability depends on the caller's trigger + actor gate`
53560
+ );
53561
+ if (forkInput && !privileged && !reusable)
53318
53562
  signals.push(
53319
53563
  "triggered by `pull_request` \u2014 fork PRs run with a read-only token (lower risk than pull_request_target)"
53320
53564
  );
@@ -53326,8 +53570,14 @@ function analyzeWorkflow(path70, content) {
53326
53570
  if (elevated) signals.push("elevated permissions (contents/id-token: write)");
53327
53571
  if (pat) signals.push("a static PAT is exposed to the agent (recoverable via injection)");
53328
53572
  if (!gate && reach > 0) signals.push("no effective actor gate");
53573
+ const noExplicitPerms = wf.permissions == null && jobList(wf).every((j) => j.permissions == null);
53574
+ if (noExplicitPerms && reach > 0 && (broadTools || githubWriteTool))
53575
+ signals.push(
53576
+ "no explicit `permissions:` \u2014 the token defaults to the repo/org setting, which may grant write; set it explicitly to read-only"
53577
+ );
53329
53578
  const mitigations = [];
53330
- if (explicitGate) mitigations.push("actor-gated (maintainer/label/write-user required)");
53579
+ if (explicitGate || membershipGated)
53580
+ mitigations.push("actor-gated (maintainer/label/write-user required)");
53331
53581
  else if (implicitGate)
53332
53582
  mitigations.push("claude-code-action gates the agent to write-access users by default");
53333
53583
  if (head === "subdir") mitigations.push("untrusted head isolated in a subdir, not root");
@@ -53346,6 +53596,105 @@ function analyzeWorkflow(path70, content) {
53346
53596
  fix: head === "root" && privileged ? "Do not check out the untrusted PR head into the workspace root under a privileged trigger (pull_request_target/workflow_run) \u2014 check out the base ref, or isolate the head in a subdir (--add-dir). Add an actor gate and scope the agent tools." : head === "root" ? "This runs under `pull_request` (fork PRs get a read-only token), so the head checkout is low-risk today \u2014 keep it on `pull_request` (not `pull_request_target`) and keep the actor gate + scoped tools." : "Add/verify an actor gate, scope the agent tools to read-only, and env-deny secrets. See Anthropic\u2019s claude-code-action security doc."
53347
53597
  };
53348
53598
  }
53599
+ var AGENT_FUEL_RE = /^(ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|ANTHROPIC_BASE_URL|CLAUDE_CODE_OAUTH_TOKEN|OPENAI_API_KEY|OPENAI_BASE_URL|GEMINI_API_KEY|GOOGLE_API_KEY|GITHUB_TOKEN)$/i;
53600
+ var FUEL_INPUT_KEYS = /^(anthropic_api_key|anthropic_auth_token|anthropic_base_url|claude_code_oauth_token|openai_api_key|openai_base_url|gemini_api_key|google_api_key)$/i;
53601
+ function fuelSecretNames(agentSteps) {
53602
+ const out = /* @__PURE__ */ new Set();
53603
+ const add = (v) => {
53604
+ for (const m of str(v).matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/gi)) out.add(m[1]);
53605
+ };
53606
+ for (const st of agentSteps) {
53607
+ for (const [k, v] of Object.entries(st.with ?? {})) if (FUEL_INPUT_KEYS.test(k)) add(v);
53608
+ for (const [k, v] of Object.entries(st.env ?? {})) if (AGENT_FUEL_RE.test(k)) add(v);
53609
+ }
53610
+ return out;
53611
+ }
53612
+ function classifySecret(name) {
53613
+ if (/AWS_|AZURE_|GCP_|GOOGLE_APPLICATION|GCLOUD/i.test(name)) return "cloud";
53614
+ if (/_PAT\b|PAT$|_TOKEN$|GH_TOKEN/i.test(name)) return "pat";
53615
+ if (/DATABASE|_DB_|POSTGRES|MYSQL|REDIS|MONGO|CONNECTION_STRING/i.test(name)) return "db";
53616
+ if (/API_KEY|_KEY$|SECRET|PASSWORD|PASSWD/i.test(name)) return "api-key";
53617
+ return "generic";
53618
+ }
53619
+ function agentReachableSecrets(wf, agentSteps, agentJobs) {
53620
+ const blobs = [];
53621
+ for (const st of agentSteps) blobs.push(str(st.env), str(st.with));
53622
+ for (const j of agentJobs) blobs.push(str(j.env));
53623
+ blobs.push(str(wf.env));
53624
+ const fuel = fuelSecretNames(agentSteps);
53625
+ const found = /* @__PURE__ */ new Map();
53626
+ for (const b of blobs) {
53627
+ for (const m of b.matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/gi)) {
53628
+ const name = m[1];
53629
+ if (AGENT_FUEL_RE.test(name) || fuel.has(name)) continue;
53630
+ found.set(name, classifySecret(name));
53631
+ }
53632
+ }
53633
+ const idToken = hasIdTokenWrite(wf, agentJobs);
53634
+ const out = [...found].map(([name, kind]) => ({ name, kind }));
53635
+ if (idToken) out.push({ name: "id-token (cloud OIDC)", kind: "cloud-oidc" });
53636
+ return out;
53637
+ }
53638
+ function evalAgentJob(job, wf, raw, untrustedTrigger, reusable) {
53639
+ const jobSteps = job.steps ?? [];
53640
+ const jobAgentSteps = jobSteps.filter(isAgentStep);
53641
+ if (jobAgentSteps.length === 0) return null;
53642
+ const secrets = agentReachableSecrets(wf, jobAgentSteps, [job]);
53643
+ if (secrets.length === 0) return null;
53644
+ const nonWriteStar = str(jobAgentSteps.map((s) => s.with?.["allowed_non_write_users"]).find(Boolean)) === "*";
53645
+ const bypassActive = nonWriteStar && untrustedTrigger;
53646
+ const head = untrustedHeadCheckout(jobSteps);
53647
+ const reach = Math.max(
53648
+ head === "root" ? 3 : head === "subdir" ? 1 : 0,
53649
+ promptTakesUntrusted(jobAgentSteps) ? 2 : 0,
53650
+ bypassActive ? 2 : 0
53651
+ );
53652
+ const gate = jobActorGate(job, wf, raw) || hasImplicitActorGate(jobAgentSteps, bypassActive);
53653
+ const injectable = untrustedTrigger && !gate && reach > 0;
53654
+ const canReadEnv = EXFIL_RCE_RE.test(collectTools(jobAgentSteps));
53655
+ const realSecrets = secrets.filter((s) => s.kind !== "cloud-oidc");
53656
+ const hasOidc = secrets.some((s) => s.kind === "cloud-oidc");
53657
+ let severity;
53658
+ if (injectable && canReadEnv) {
53659
+ severity = hasOidc || realSecrets.some((s) => ["cloud", "pat", "db"].includes(s.kind)) ? "critical" : "high";
53660
+ } else if (realSecrets.length > 0) {
53661
+ severity = "advisory";
53662
+ } else {
53663
+ return null;
53664
+ }
53665
+ const loadedGun = head === "root" || promptTakesUntrusted(jobAgentSteps);
53666
+ if (reusable && !loadedGun && SEVERITY_RANK2[severity] > SEVERITY_RANK2.medium) severity = "medium";
53667
+ return { severity, secrets, injectable, canReadEnv };
53668
+ }
53669
+ function analyzeWorkflowSecrets(path70, content) {
53670
+ let raw;
53671
+ try {
53672
+ raw = (0, import_yaml.parse)(content) ?? {};
53673
+ } catch {
53674
+ return null;
53675
+ }
53676
+ const wf = raw;
53677
+ const all = allSteps(wf);
53678
+ if (!all.some((s) => isAgentStep(s.step))) return null;
53679
+ const { untrusted: forkInput, reusable } = triggerReach(wf, raw);
53680
+ const untrustedTrigger = forkInput || reusable;
53681
+ const agentJobs = [...new Set(all.filter((s) => isAgentStep(s.step)).map((s) => s.job))];
53682
+ const worst = agentJobs.map((job) => evalAgentJob(job, wf, raw, untrustedTrigger, reusable)).filter((e) => e !== null).sort((a, b) => SEVERITY_RANK2[b.severity] - SEVERITY_RANK2[a.severity])[0];
53683
+ if (!worst) return null;
53684
+ return {
53685
+ check: "CI-4",
53686
+ dimension: "data",
53687
+ severity: worst.severity,
53688
+ title: worst.severity === "advisory" ? "Secrets reachable by the agent \u2014 hardening" : "Exfiltratable secrets reachable by an injectable agent",
53689
+ file: path70,
53690
+ signals: [
53691
+ `agent can reach: ${worst.secrets.map((s) => s.name).join(", ")}`,
53692
+ worst.injectable ? "the agent is externally triggerable (untrusted trigger, no gate)" : "gated / not externally triggerable \u2014 latent risk only",
53693
+ worst.canReadEnv ? "agent has arbitrary shell (bare Bash) \u2192 can read env and exfiltrate" : "no arbitrary-shell tool \u2014 not exfiltratable today, but one tool-add away"
53694
+ ],
53695
+ fix: "Move extra secrets to a separate trusted job the agent cannot reach; drop id-token:write if unused; scope the agent tools to read-only and gate the trigger."
53696
+ };
53697
+ }
53349
53698
 
53350
53699
  // src/ci-check/agent-config.ts
53351
53700
  function asStrings(v) {
@@ -53373,18 +53722,20 @@ function analyzeAgentConfig(path70, content) {
53373
53722
  }
53374
53723
  const findings = [];
53375
53724
  for (const cmd of hookCommands(cfg.hooks)) {
53376
- const remote = /\b(npx|curl|wget|iwr|irm)\b/.test(cmd) || /\|\s*(sh|bash)\b/.test(cmd);
53377
- if (!remote) continue;
53378
- const unpinned = /@latest\b/.test(cmd) || /\bnpx\b/.test(cmd) && !/@\d/.test(cmd);
53725
+ const remoteExec = /\|\s*(sh|bash|zsh)\b/.test(cmd) || /\b(curl|wget|iwr|irm)\b/.test(cmd);
53726
+ const isNpx = /\bnpx\b/.test(cmd);
53727
+ if (!remoteExec && !isNpx) continue;
53728
+ const unpinned = /@latest\b/.test(cmd) || isNpx && !/@\d/.test(cmd);
53729
+ const high = remoteExec || unpinned;
53379
53730
  findings.push({
53380
53731
  check: "CI-1",
53381
53732
  dimension: "toolRules",
53382
- severity: unpinned ? "high" : "medium",
53383
- title: unpinned ? "Agent hook runs UNPINNED third-party code on every action" : "Agent hook runs third-party code in the agent hot path",
53733
+ severity: high ? "high" : "medium",
53734
+ title: high ? "Agent hook runs UNPINNED/remote third-party code on every action" : "Agent hook runs third-party code in the agent hot path",
53384
53735
  file: path70,
53385
53736
  signals: [
53386
53737
  `hook command: \`${cmd.slice(0, 120)}\``,
53387
- unpinned ? "unpinned \u2014 a compromised/yanked package = code execution on every contributor" : "pinned, but still a standing supply-chain dependency in the agent hot path"
53738
+ remoteExec ? "fetch-and-run (curl|wget / pipe-to-shell) \u2014 unpinnable remote code execution on every contributor" : unpinned ? "unpinned \u2014 a compromised/yanked package = code execution on every contributor" : "pinned, but still a standing supply-chain dependency in the agent hot path"
53388
53739
  ],
53389
53740
  fix: "Vendor the command as a committed local script, or pin an exact version and treat updates as security-reviewed."
53390
53741
  });
@@ -53395,17 +53746,18 @@ function analyzeAgentConfig(path70, content) {
53395
53746
  (a) => /^Bash$|^Bash\(\s*\*|^Bash\(git:|^Write\(\s*\*|^Write$|^Edit$/.test(a)
53396
53747
  );
53397
53748
  if (broad.length > 0) {
53749
+ const hasBackstop = deny.some((d) => /Bash|Write|Edit/.test(d));
53398
53750
  findings.push({
53399
53751
  check: "CI-1",
53400
53752
  dimension: "toolRules",
53401
- severity: "medium",
53402
- title: "Committed agent config pre-authorizes broad tools",
53753
+ severity: hasBackstop ? "medium" : "high",
53754
+ title: hasBackstop ? "Committed agent config pre-authorizes broad tools" : "Committed agent config pre-authorizes broad tools with no deny backstop",
53403
53755
  file: path70,
53404
53756
  signals: [
53405
53757
  `broad allow(s): ${broad.slice(0, 5).join(", ")}`,
53406
- ...deny.length === 0 ? ["no `deny` entries to backstop it"] : []
53758
+ hasBackstop ? "a `deny` list backstops the broad allow" : "no `deny` entry covers Bash/Write/Edit \u2014 every contributor is pre-authorized for catastrophic tools"
53407
53759
  ],
53408
- fix: "Scope the allow-list to specific read-only subcommands (e.g. `Bash(gh pr view:*)`); avoid bare `Bash`/`git:`/`Write`."
53760
+ fix: "Scope the allow-list to specific read-only subcommands (e.g. `Bash(gh pr view:*)`); avoid bare `Bash`/`git:`/`Write`, or add a `deny` backstop."
53409
53761
  });
53410
53762
  }
53411
53763
  return findings;
@@ -53420,8 +53772,11 @@ function analyzeMcp(path70, content) {
53420
53772
  } catch {
53421
53773
  return [];
53422
53774
  }
53775
+ return analyzeMcpServers(cfg.mcpServers ?? {}, path70);
53776
+ }
53777
+ function analyzeMcpServers(servers, path70) {
53423
53778
  const findings = [];
53424
- for (const [name, srv] of Object.entries(cfg.mcpServers ?? {})) {
53779
+ for (const [name, srv] of Object.entries(servers ?? {})) {
53425
53780
  if (!srv || srv.disabled) continue;
53426
53781
  const argv = [srv.command, ...Array.isArray(srv.args) ? srv.args.map(String) : []].join(" ");
53427
53782
  if (/\bnpx\b/.test(argv) && (/@latest\b/.test(argv) || !/@\d/.test(argv))) {
@@ -53456,6 +53811,199 @@ function analyzeMcp(path70, content) {
53456
53811
  return findings;
53457
53812
  }
53458
53813
 
53814
+ // src/ci-check/codex.ts
53815
+ var import_smol_toml5 = require("smol-toml");
53816
+ function analyzeCodexConfig(path70, content) {
53817
+ let cfg;
53818
+ try {
53819
+ cfg = (0, import_smol_toml5.parse)(content);
53820
+ } catch {
53821
+ return [];
53822
+ }
53823
+ const findings = [];
53824
+ findings.push(...analyzeMcpServers(cfg.mcp_servers ?? {}, path70));
53825
+ const sandbox = typeof cfg.sandbox_mode === "string" ? cfg.sandbox_mode : "";
53826
+ const approval = typeof cfg.approval_policy === "string" ? cfg.approval_policy : "";
53827
+ const fullAccess = /danger-full-access/i.test(sandbox);
53828
+ const noApproval = /^never$/i.test(approval);
53829
+ if (fullAccess || noApproval) {
53830
+ const signals = [
53831
+ fullAccess ? 'sandbox_mode = "danger-full-access" \u2014 the agent runs arbitrary commands with full disk + network access' : null,
53832
+ noApproval ? 'approval_policy = "never" \u2014 no human approval for agent actions' : null
53833
+ ].filter((s) => s !== null);
53834
+ findings.push({
53835
+ check: "CI-1",
53836
+ dimension: "toolRules",
53837
+ severity: fullAccess ? "high" : "medium",
53838
+ title: fullAccess ? "Codex config grants a full-access sandbox" : "Codex config never requires approval",
53839
+ file: path70,
53840
+ signals,
53841
+ fix: 'Commit a least-privilege Codex config: prefer `sandbox_mode = "read-only"` (or `"workspace-write"`) and `approval_policy = "on-request"`/`"on-failure"`. A repo-committed config applies to every contributor who runs Codex here.'
53842
+ });
53843
+ }
53844
+ return findings;
53845
+ }
53846
+
53847
+ // src/ci-check/instructions.ts
53848
+ var TAG_CHARS = /[\u{E0000}-\u{E007F}]/u;
53849
+ var BIDI_OVERRIDE = /[‭‮]/;
53850
+ var BIDI_EMBED_ISOLATE = /[‪-‬⁦-⁩]/;
53851
+ function isZwLegitScript(cp) {
53852
+ if (cp === void 0) return false;
53853
+ return cp >= 3584 && cp <= 3711 || cp >= 3712 && cp <= 3839 || cp >= 4096 && cp <= 4255 || cp >= 6016 && cp <= 6143 || cp >= 12352 && cp <= 12543 || cp >= 13312 && cp <= 40959 || cp >= 44032 && cp <= 55215;
53854
+ }
53855
+ var isAsciiWordChar = (ch) => !!ch && /[A-Za-z0-9]/.test(ch);
53856
+ function suspiciousZeroWidth(text) {
53857
+ let n = 0;
53858
+ for (let i = 0; i < text.length; i++) {
53859
+ const c = text.charCodeAt(i);
53860
+ if (c !== 8203 && c !== 8288) continue;
53861
+ if (isZwLegitScript(text.codePointAt(i - 1)) || isZwLegitScript(text.codePointAt(i + 1)))
53862
+ continue;
53863
+ const prev = text[i - 1];
53864
+ const next = text[i + 1];
53865
+ if (!prev || !next || /\s/.test(prev) || /\s/.test(next)) continue;
53866
+ if (isAsciiWordChar(prev) && isAsciiWordChar(next)) n++;
53867
+ }
53868
+ return n;
53869
+ }
53870
+ var stripZeroWidth = (t) => t.replace(/[​⁠]/g, "");
53871
+ var OVERRIDE_RE = /ignore\s+(all\s+)?(previous|prior|the\s+above)\s+(instructions|prompts?|rules)|disregard\s+(the\s+|your\s+)?(system\s+)?(prompt|instructions|rules)|forget\s+(everything|all\s+(previous|prior))|you\s+are\s+now\s+(a|an|the)\b|<\/?system>/i;
53872
+ var FETCH_OBEY_RE = /\b(curl|wget|iwr|invoke-webrequest)\b[^\n|]*\|\s*(bash|sh|zsh|python3?|node|iex)\b|\b(curl|wget)\b[^\n]*&&[^\n]*\b(bash|sh)\b/i;
53873
+ var SECRET_PATH_RE = /~\/\.aws\/credentials|~\/\.ssh\/id_[a-z]+|~\/\.config\/gh\/hosts|read\s+the\s+(token|secret|api[_ ]?key|password)\s+(in|from)\s+[.`'"]?\.?env/i;
53874
+ var EXFIL_RE = /\b(post|send|upload|exfiltrate|forward)\b[^\n]{0,40}\b(to|at)\b[^\n]{0,50}(https?:\/\/|webhook|hook\.[a-z])/i;
53875
+ var HUMAN_SECTION_RE = /^#+\s*(install|installation|setup|set ?up|getting started|quick ?start|contributing|contribution|development|dev setup|build|prerequisites|requirements|usage)\b/i;
53876
+ var NEGATION_RE = /\b(never|do not|don'?t|avoid|must not|should not|no need to|refuse to)\b/i;
53877
+ function isNegated(text, idx) {
53878
+ return NEGATION_RE.test(text.slice(Math.max(0, idx - 40), idx));
53879
+ }
53880
+ function inHumanSection(text, idx) {
53881
+ const heading = text.slice(0, idx).split("\n").reverse().find((l) => /^#+\s/.test(l));
53882
+ return !!heading && HUMAN_SECTION_RE.test(heading);
53883
+ }
53884
+ function decodeSuspiciousBase64(text) {
53885
+ let out = "";
53886
+ for (const m of text.matchAll(/[A-Za-z0-9+/]{40,}={0,2}/g)) {
53887
+ try {
53888
+ const d = Buffer.from(m[0], "base64").toString("utf8");
53889
+ if (/[\x20-\x7E]{16,}/.test(d) && /[a-z]{4,}/i.test(d)) out += " " + d;
53890
+ } catch {
53891
+ }
53892
+ }
53893
+ return out;
53894
+ }
53895
+ function mk(severity, title, signals, fix, path70) {
53896
+ return { check: "CI-6", dimension: "instructions", severity, title, file: path70, signals, fix };
53897
+ }
53898
+ function analyzeInstructionFile(path70, content) {
53899
+ const findings = [];
53900
+ const decoded = decodeSuspiciousBase64(content);
53901
+ if (TAG_CHARS.test(content))
53902
+ findings.push(
53903
+ mk(
53904
+ "critical",
53905
+ "Unicode tag characters in an agent instruction file",
53906
+ [
53907
+ "contains Unicode tag characters (U+E0000\u2013E007F) \u2014 an invisible instruction-smuggling channel with no legitimate use in text"
53908
+ ],
53909
+ "Remove the tag characters. Instruction files must be plain, reviewable text.",
53910
+ path70
53911
+ )
53912
+ );
53913
+ if (BIDI_OVERRIDE.test(content))
53914
+ findings.push(
53915
+ mk(
53916
+ "critical",
53917
+ "Bidirectional override characters in an agent instruction file",
53918
+ [
53919
+ "contains a bidi override (U+202D/U+202E) \u2014 a Trojan-Source technique that visually reorders text so a human reads something different from what the agent parses"
53920
+ ],
53921
+ "Remove the bidi override characters.",
53922
+ path70
53923
+ )
53924
+ );
53925
+ else if (BIDI_EMBED_ISOLATE.test(content))
53926
+ findings.push(
53927
+ mk(
53928
+ "advisory",
53929
+ "Bidirectional formatting characters in an agent instruction file",
53930
+ [
53931
+ "contains bidi embed/isolate characters (U+202A\u2013202C / U+2066\u20132069) \u2014 legitimate in right-to-left text, but confirm they are not being used to hide or reorder instructions"
53932
+ ],
53933
+ "Confirm the bidi marks are legitimate RTL formatting; remove otherwise.",
53934
+ path70
53935
+ )
53936
+ );
53937
+ const zw = suspiciousZeroWidth(content);
53938
+ if (zw > 0) {
53939
+ const revealed = OVERRIDE_RE.test(stripZeroWidth(content)) && !OVERRIDE_RE.test(content);
53940
+ findings.push(
53941
+ mk(
53942
+ revealed ? "critical" : "medium",
53943
+ "Zero-width characters splitting text in an agent instruction file",
53944
+ [
53945
+ revealed ? "a zero-width character conceals a prompt-override directive that only appears once the hidden characters are stripped" : "a zero-width character splits a visible Latin word \u2014 a concealment technique (hides text from human review while the agent reads it as contiguous)"
53946
+ ],
53947
+ "Remove the zero-width characters. Instruction files must be plain, reviewable text.",
53948
+ path70
53949
+ )
53950
+ );
53951
+ }
53952
+ const ov = OVERRIDE_RE.exec(content);
53953
+ const ovEnc = !ov ? OVERRIDE_RE.exec(decoded) : null;
53954
+ if (ov || ovEnc) {
53955
+ const m = ov || ovEnc;
53956
+ findings.push(
53957
+ mk(
53958
+ ovEnc ? "critical" : "high",
53959
+ "Prompt-override directive in an agent instruction file",
53960
+ [
53961
+ `contains a prompt-override / role-impersonation directive (\`${m[0].slice(0, 60).trim()}\`)${ovEnc ? " \u2014 concealed in a base64 blob" : ""}`
53962
+ ],
53963
+ "Remove the override text. An instruction file should not tell the agent to ignore its own rules.",
53964
+ path70
53965
+ )
53966
+ );
53967
+ }
53968
+ const fo = FETCH_OBEY_RE.exec(content);
53969
+ if (fo && !inHumanSection(content, fo.index) && !isNegated(content, fo.index)) {
53970
+ findings.push(
53971
+ mk(
53972
+ "medium",
53973
+ "Instruction directs the agent to fetch and run remote code",
53974
+ [`\`${fo[0].slice(0, 70).trim()}\` \u2014 fetch-and-obey, outside an install/setup section`],
53975
+ "Do not instruct the agent to pipe remote content into a shell; pin and vendor scripts instead.",
53976
+ path70
53977
+ )
53978
+ );
53979
+ }
53980
+ const sp = SECRET_PATH_RE.exec(content);
53981
+ if (sp && !isNegated(content, sp.index)) {
53982
+ findings.push(
53983
+ mk(
53984
+ "medium",
53985
+ "Instruction points the agent at credential material",
53986
+ [`references \`${sp[0].slice(0, 50).trim()}\` \u2014 directs the agent toward secrets`],
53987
+ "Do not reference credential files or paths in agent instructions.",
53988
+ path70
53989
+ )
53990
+ );
53991
+ }
53992
+ const ex = EXFIL_RE.exec(content);
53993
+ if (ex && !inHumanSection(content, ex.index) && !isNegated(content, ex.index)) {
53994
+ findings.push(
53995
+ mk(
53996
+ "medium",
53997
+ "Instruction directs the agent to send data to an external endpoint",
53998
+ [`\`${ex[0].slice(0, 70).trim()}\` \u2014 possible exfiltration directive`],
53999
+ "Remove external post/upload directives from agent instructions.",
54000
+ path70
54001
+ )
54002
+ );
54003
+ }
54004
+ return findings;
54005
+ }
54006
+
53459
54007
  // src/ci-check/index.ts
53460
54008
  function worstOf(findings) {
53461
54009
  let worst = null;
@@ -53474,10 +54022,18 @@ function scanTree(tree) {
53474
54022
  if (/\.github\/workflows\/.+\.ya?ml$/.test(file.path)) {
53475
54023
  const f = analyzeWorkflow(file.path, file.content);
53476
54024
  if (f) findings.push(f);
54025
+ const s = analyzeWorkflowSecrets(file.path, file.content);
54026
+ if (s) findings.push(s);
53477
54027
  } else if (/\.claude\/settings(\.local)?\.json$/.test(file.path)) {
53478
54028
  findings.push(...analyzeAgentConfig(file.path, file.content));
53479
54029
  } else if (/\.mcp\.json$|\.cursor\/mcp\.json$/.test(file.path)) {
53480
54030
  findings.push(...analyzeMcp(file.path, file.content));
54031
+ } else if (/(^|\/)\.codex\/config\.toml$/.test(file.path)) {
54032
+ findings.push(...analyzeCodexConfig(file.path, file.content));
54033
+ } else if (/(^|\/)(CLAUDE|AGENTS|GEMINI)\.md$|(^|\/)\.cursorrules$|copilot-instructions\.md$|(^|\/)\.(windsurf|cline)rules$/.test(
54034
+ file.path
54035
+ )) {
54036
+ findings.push(...analyzeInstructionFile(file.path, file.content));
53481
54037
  }
53482
54038
  } catch (err2) {
53483
54039
  notes.push(`checker degraded on ${file.path}: ${err2?.message ?? "error"}`);
@@ -53508,6 +54064,36 @@ var COLOR = {
53508
54064
  medium: import_chalk29.default.yellow,
53509
54065
  advisory: import_chalk29.default.gray
53510
54066
  };
54067
+ var ACTION_URL = "https://github.com/marketplace/actions/node9-agent-security-check?ref=cli_scan_repo";
54068
+ function renderCta(res) {
54069
+ const L = [];
54070
+ L.push(import_chalk29.default.dim(" " + "\u2500".repeat(63)));
54071
+ if (res.worst === "critical" || res.worst === "high") {
54072
+ const n = res.findings.filter((f) => f.severity === "critical" || f.severity === "high").length;
54073
+ L.push(
54074
+ " " + import_chalk29.default.red.bold(
54075
+ `\u{1F534} ${n} ${n === 1 ? "issue" : "issues"} to fix \u2014 then stop the next at the PR.`
54076
+ )
54077
+ );
54078
+ L.push("");
54079
+ L.push(" " + import_chalk29.default.bold("Catch this class of issue on every PR, automatically:"));
54080
+ } else if (res.worst) {
54081
+ L.push(" " + import_chalk29.default.yellow("\u{1F7E1} Review the findings above, then keep it covered:"));
54082
+ L.push("");
54083
+ L.push(" " + import_chalk29.default.bold("Check every PR for agent-CI risk:"));
54084
+ } else if (res.incomplete) {
54085
+ L.push(" " + import_chalk29.default.yellow.bold("\u26A0\uFE0F Incomplete \u2014 not a clean bill of health."));
54086
+ L.push("");
54087
+ L.push(" " + import_chalk29.default.bold("Get a complete check on every PR (CI reads the tree directly):"));
54088
+ } else {
54089
+ L.push(" " + import_chalk29.default.green("\u2705 Agent CI is well-configured \u2014 0 unmitigated issues."));
54090
+ L.push("");
54091
+ L.push(" " + import_chalk29.default.bold("Keep it green as you add agent workflows \u2014 check every PR:"));
54092
+ }
54093
+ L.push(" " + import_chalk29.default.dim("\u2192 ") + import_chalk29.default.cyan.underline(ACTION_URL));
54094
+ L.push(" " + import_chalk29.default.gray(" zero setup \xB7 no token \xB7 runs in your CI"));
54095
+ return L;
54096
+ }
53511
54097
  function ownedHint(source) {
53512
54098
  return source.startsWith("/") || source.startsWith(".") || source.startsWith("~");
53513
54099
  }
@@ -53552,6 +54138,8 @@ function renderScan(res) {
53552
54138
  )
53553
54139
  );
53554
54140
  }
54141
+ L.push("");
54142
+ L.push(...renderCta(res));
53555
54143
  return L.join("\n");
53556
54144
  }
53557
54145
  function renderScanMarkdown(res) {