Mostly safe โ a couple of notes worth reading.
Scanned 10/30/2026, 7:06:24 AMยทCached resultยทDeep Scanยท91 rulesยทHow we decide โ
AIVSS Score
Low
Severity Breakdown
0
critical
1
high
21
medium
0
low
MCP Server Information
Findings
This package has a B grade with a safety score of 68/100 and carries one high-severity vulnerability along with 21 medium-severity issues, primarily related to server configuration and one ansi escape injection flaw. The configuration weaknesses suggest potential exposure to misuse or improper deployment, though no critical vulnerabilities were detected. You should address the high-severity finding and review the server configuration recommendations before deploying this in production.
AIPer-finding remediation generated by bedrock-claude-haiku-4-5 โ 20 of 22 findings. Click any finding to read.
No known CVEs found for this package or its dependencies.
Scan Details
Done
Sign in to save scan history and re-scan automatically on new commits.
Building your own MCP server?
Same rules, same LLM judges, same grade. Private scans stay isolated to your account and never appear in the public registry. Required for code your team hasnโt shipped yet.
22 of 22 findings
22 findings
MCP prompt handler returns a template that interpolates an untrusted handler argument (f-string `{x}`, template-literal `${x}`, string concatenation, or `.format()`/`%` formatting) into the prompt text. The host LLM that consumes this prompt via `prompts/get` will read the attacker-controlled text as part of its instructions โ pivot to arbitrary LLM behaviour. Wrap the parameter in `<untrusted>...</untrusted>`, run it through `escape_for_prompt` / `sanitize_prompt`, or pass via `json.dumps` / `
Evidence
| 1 | import { z } from 'zod'; |
| 2 | import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; |
| 3 | |
| 4 | export function registerGuardPrompts(server: McpServer): void { |
| 5 | server.registerPrompt( |
| 6 | 'codapult_guard_proposal_review', |
| 7 | { |
| 8 | title: 'Guard Proposal Review', |
| 9 | description: 'Refine evidence-backed Guard rule and contract proposals before activation.', |
| 10 | argsSchema: { |
| 11 | proposals: z.string().describe('JSON proposal packet returned by codapult_guard_propose'), |
| 12 | focu |
RemediationAI
The problem is that `src/mcp/prompts.ts` interpolates an untrusted handler argument directly into the prompt template using f-string or template-literal syntax, allowing an attacker to inject arbitrary instructions into the LLM's context. Wrap the parameter value in `<untrusted>...</untrusted>` XML tags or pass it through a dedicated `sanitize_prompt()` function before template interpolation, ensuring the LLM treats it as data rather than instructions. This fix prevents prompt injection by explicitly marking user input as untrusted data that the LLM should not execute. Verify by passing a test payload containing LLM directives (e.g., `"ignore previous instructions"`) and confirm the prompt output wraps it in tags or escapes special characters.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 165 | recoverable: true, |
| 166 | hint: 'Repair or remove the invalid Guard artifact, then run `codapult-guard doctor`.', |
| 167 | }); |
| 168 | if (options.json || options.machine) console.log(JSON.stringify(payload, null, 2)); |
| 169 | else fail(`${payload.message} Run \`codapult-guard doctor\` for details.`); |
| 170 | process.exitCode = 1; |
| 171 | return { invalid: true }; |
RemediationAI
The problem is that `src/cli/commands/guard.ts` prints `payload.message` directly to the console without sanitizing ANSI escape sequences, allowing an attacker to inject cursor-control codes that rewrite terminal output or hide commands. Replace the `fail()` call with a sanitized version that strips or escapes ANSI sequencesโuse a library like `strip-ansi` or manually remove `\x1b[` patterns before logging. This fix prevents terminal hijacking and output spoofing by ensuring only safe printable characters reach the console. Test by passing a message containing ANSI codes (e.g., `"\x1b[2J"` to clear screen) and verify the output displays the escape sequence literally rather than executing it.
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 173 | tooling: z |
| 174 | .record( |
| 175 | z.string().trim().min(1), |
| 176 | z.object({ script: z.string().trim().min(1), enabled: z.boolean() }), |
| 177 | ) |
| 178 | .default({}), |
| 179 | completionGate: z.object({ |
RemediationAI
The problem is that `src/core/policy/schemas.ts` defines a `script` field as an unconstrained string, allowing callers to pass arbitrary shell commands or code that will be executed with the tool's privileges. Narrow the schema by adding `.max(length)`, `.regex(pattern)`, or `.enum([...])` constraints to the `script` fieldโfor example, `.string().trim().min(1).max(500).regex(/^[a-zA-Z0-9_\-./]+$/)` to allow only safe filenames or identifiers. This fix reduces the blast radius by restricting the input space to known-safe values, preventing injection of malicious commands. Verify by attempting to pass a script containing shell metacharacters (e.g., `"; rm -rf /";`) and confirm the schema validation rejects it.
MCP manifest declares tools but no authentication field is present (none of: auth, authorization, bearer, oauth, mtls, apiKey, api_key, basic, token, authToken). Absence is a weak signal โ confirm whether the server relies on network-layer or host-level auth, or declare the real mechanism explicitly so reviewers can audit it.
Evidence
| 1 | # Cursor integration |
| 2 | |
| 3 | This page targets Cursor Agent in a trusted local workspace. Cursor supports project MCP |
| 4 | configuration in `.cursor/mcp.json`, project rules in `.cursor/rules/`, and project hooks in |
| 5 | `.cursor/hooks.json`. |
| 6 | |
| 7 | ## 1. Install Guard |
| 8 | |
| 9 | Guard is listed in the official MCP Registry as `io.github.codapult/guard`. In a client that can |
| 10 | import Registry entries, search for that identifier and select the npm/stdio server. Cursor's |
| 11 | native MCP directory and one-click install are separate from |
RemediationAI
The problem is that the MCP manifest in `docs/integrations/cursor.md` does not declare an authentication mechanism, leaving reviewers unable to verify whether the server is protected against unauthorized access. Add an explicit `auth` or `authorization` field to the manifest (or a comment in the documentation) stating the authentication methodโe.g., `"auth": "local-only"` or `"auth": "host-level-isolation"` if the server relies on filesystem permissions or process isolation. This fix clarifies the security boundary and allows auditors to assess whether the authentication model is appropriate for the deployment context. Verify by reviewing the manifest schema and confirming that the declared auth method matches the actual access controls in the server code.
MCP manifest declares tools but no authentication field is present (none of: auth, authorization, bearer, oauth, mtls, apiKey, api_key, basic, token, authToken). Absence is a weak signal โ confirm whether the server relies on network-layer or host-level auth, or declare the real mechanism explicitly so reviewers can audit it.
Evidence
| 1 | # Codapult Guard: Architecture Guardrails for AI Coding Agents |
| 2 | |
| 3 | Codapult Guard is local-first architecture guardrail software for AI coding agents and JavaScript |
| 4 | or TypeScript repositories. It detects architectural drift by observing the existing software |
| 5 | architecture, recording project memory, and checking code changes against team-approved rules and |
| 6 | contracts. It also provides bounded project context for AI code review and deterministic checks for |
| 7 | local development and CI. |
| 8 | |
| 9 | Guard is not a repl |
RemediationAI
The problem is that the MCP manifest in `docs/guard.md` does not declare an authentication mechanism, leaving reviewers unable to verify whether the server is protected against unauthorized access. Add an explicit `auth` or `authorization` field to the manifest (or a comment in the documentation) stating the authentication methodโe.g., `"auth": "local-only"` or `"auth": "host-level-isolation"` if the server relies on filesystem permissions or process isolation. This fix clarifies the security boundary and allows auditors to assess whether the authentication model is appropriate for the deployment context. Verify by reviewing the manifest schema and confirming that the declared auth method matches the actual access controls in the server code.
MCP manifest declares tools but no authentication field is present (none of: auth, authorization, bearer, oauth, mtls, apiKey, api_key, basic, token, authToken). Absence is a weak signal โ confirm whether the server relies on network-layer or host-level auth, or declare the real mechanism explicitly so reviewers can audit it.
Evidence
| 1 | # @codapult/guard |
| 2 | |
| 3 | [](https://github.com/codapult/codapult-guard/actions/workflows/ci.yml) |
| 4 | [](https://github.com/codapult/codapult-guard/actions/workflows/guard-fixtures.yml) |
| 5 | [](https://www.npmjs.com/package/@codapult/guard) |
| 6 | [ stating the authentication methodโe.g., `"auth": "local-only"` or `"auth": "host-level-isolation"` if the server relies on filesystem permissions or process isolation. This fix clarifies the security boundary and allows auditors to assess whether the authentication model is appropriate for the deployment context. Verify by reviewing the manifest schema and confirming that the declared auth method matches the actual access controls in the server code.
MCP manifest declares tools but no authentication field is present (none of: auth, authorization, bearer, oauth, mtls, apiKey, api_key, basic, token, authToken). Absence is a weak signal โ confirm whether the server relies on network-layer or host-level auth, or declare the real mechanism explicitly so reviewers can audit it.
Evidence
| 1 | # GitHub Copilot integration |
| 2 | |
| 3 | GitHub Copilot has two relevant local surfaces. Use the VS Code instructions for Copilot Chat in Agent mode, or the Copilot CLI instructions for the terminal agent. They do not share the same MCP configuration file. |
| 4 | |
| 5 | ## Copilot Chat in VS Code |
| 6 | |
| 7 | Guard is published in the official MCP Registry as `io.github.codapult/guard`. Registry import is client-dependent; if it is not available in your VS Code MCP gallery, configure the server manually. |
| 8 | |
| 9 | Install the package: |
| 10 | |
| 11 | ``` |
RemediationAI
The problem is that the MCP manifest in `docs/integrations/github-copilot.md` does not declare an authentication mechanism, leaving reviewers unable to verify whether the server is protected against unauthorized access. Add an explicit `auth` or `authorization` field to the manifest (or a comment in the documentation) stating the authentication methodโe.g., `"auth": "local-only"` or `"auth": "host-level-isolation"` if the server relies on filesystem permissions or process isolation. This fix clarifies the security boundary and allows auditors to assess whether the authentication model is appropriate for the deployment context. Verify by reviewing the manifest schema and confirming that the declared auth method matches the actual access controls in the server code.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 18 | runs-on: ubuntu-latest |
| 19 | steps: |
| 20 | - name: Checkout |
| 21 | uses: actions/checkout@v6 |
| 22 | |
| 23 | - name: Install pnpm |
| 24 | uses: pnpm/action-setup@v5 |
RemediationAI
The problem is that `.github/workflows/ci.yml` uses `actions/checkout@v6` which is a mutable tag that can be rewritten by a compromised maintainer, allowing silent injection of malicious code into the CI pipeline. Replace `actions/checkout@v6` with a pinned 40-character commit SHA, e.g., `actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v6.1.0`, using tools like `pinact` or `ratchet` to automate the process. This fix ensures that the exact version of the action is immutable and cannot be silently replaced without explicit code review. Verify by running `git log --oneline actions/checkout` to confirm the SHA corresponds to the intended version tag.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 21 | uses: pnpm/action-setup@v5 |
| 22 | |
| 23 | - name: Setup Node.js |
| 24 | uses: actions/setup-node@v6 |
| 25 | with: |
| 26 | node-version: 24 |
| 27 | cache: pnpm |
RemediationAI
The problem is that `.github/workflows/guard-fixtures.yml` uses `pnpm/action-setup@v5` and `actions/setup-node@v6` which are mutable tags that can be rewritten by a compromised maintainer, allowing silent injection of malicious code into the CI pipeline. Replace these with pinned 40-character commit SHAs, e.g., `pnpm/action-setup@c3ca66f6d009969d338f8301cb08112290e51338 # v5` and `actions/setup-node@60edb5dd545a775178fbb8726d51985e50cf05b2 # v6`, using tools like `pinact` or `ratchet` to automate the process. This fix ensures that the exact versions of the actions are immutable and cannot be silently replaced without explicit code review. Verify by running `git log --oneline` on each action's repository to confirm the SHAs correspond to the intended version tags.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 15 | runs-on: ubuntu-latest |
| 16 | steps: |
| 17 | - name: Checkout Guard |
| 18 | uses: actions/checkout@v6 |
| 19 | |
| 20 | - name: Install pnpm |
| 21 | uses: pnpm/action-setup@v5 |
RemediationAI
The problem is that `.github/workflows/guard-fixtures.yml` uses `actions/checkout@v6` which is a mutable tag that can be rewritten by a compromised maintainer, allowing silent injection of malicious code into the CI pipeline. Replace `actions/checkout@v6` with a pinned 40-character commit SHA, e.g., `actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v6.1.0`, using tools like `pinact` or `ratchet` to automate the process. This fix ensures that the exact version of the action is immutable and cannot be silently replaced without explicit code review. Verify by running `git log --oneline actions/checkout` to confirm the SHA corresponds to the intended version tag.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 13 | runs-on: ubuntu-latest |
| 14 | steps: |
| 15 | - uses: actions/checkout@v6 |
| 16 | - uses: pnpm/action-setup@v5 |
| 17 | - uses: actions/setup-node@v6 |
| 18 | with: |
| 19 | node-version: 24 |
RemediationAI
The problem is that `.github/workflows/guard-benchmarks.yml` uses `actions/checkout@v6`, `pnpm/action-setup@v5`, and `actions/setup-node@v6` which are mutable tags that can be rewritten by a compromised maintainer, allowing silent injection of malicious code into the CI pipeline. Replace these with pinned 40-character commit SHAs, e.g., `actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v6.1.0`, `pnpm/action-setup@c3ca66f6d009969d338f8301cb08112290e51338 # v5`, and `actions/setup-node@60edb5dd545a775178fbb8726d51985e50cf05b2 # v6`, using tools like `pinact` or `ratchet` to automate the process. This fix ensures that the exact versions of the actions are immutable and cannot be silently replaced without explicit code review. Verify by running `git log --oneline` on each action's repository to confirm the SHAs correspond to the intended version tags.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 24 | uses: pnpm/action-setup@v5 |
| 25 | |
| 26 | - name: Setup Node.js |
| 27 | uses: actions/setup-node@v6 |
| 28 | with: |
| 29 | node-version: 22 |
| 30 | cache: pnpm |
RemediationAI
The problem is that `.github/workflows/ci.yml` uses `pnpm/action-setup@v5` and `actions/setup-node@v6` which are mutable tags that can be rewritten by a compromised maintainer, allowing silent injection of malicious code into the CI pipeline. Replace these with pinned 40-character commit SHAs, e.g., `pnpm/action-setup@c3ca66f6d009969d338f8301cb08112290e51338 # v5` and `actions/setup-node@60edb5dd545a775178fbb8726d51985e50cf05b2 # v6`, using tools like `pinact` or `ratchet` to automate the process. This fix ensures that the exact versions of the actions are immutable and cannot be silently replaced without explicit code review. Verify by running `git log --oneline` on each action's repository to confirm the SHAs correspond to the intended version tags.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 30 | ref: ${{ inputs.tag || github.event.release.tag_name }} |
| 31 | |
| 32 | - name: Install pnpm |
| 33 | uses: pnpm/action-setup@v5 |
| 34 | |
| 35 | - name: Setup Node.js |
| 36 | uses: actions/setup-node@v6 |
RemediationAI
The problem is that `.github/workflows/release.yml` uses `pnpm/action-setup@v5` and `actions/setup-node@v6` which are mutable tags that can be rewritten by a compromised maintainer, allowing silent injection of malicious code into the CI pipeline. Replace these with pinned 40-character commit SHAs, e.g., `pnpm/action-setup@c3ca66f6d009969d338f8301cb08112290e51338 # v5` and `actions/setup-node@60edb5dd545a775178fbb8726d51985e50cf05b2 # v6`, using tools like `pinact` or `ratchet` to automate the process. This fix ensures that the exact versions of the actions are immutable and cannot be silently replaced without explicit code review. Verify by running `git log --oneline` on each action's repository to confirm the SHAs correspond to the intended version tags.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 18 | uses: actions/checkout@v6 |
| 19 | |
| 20 | - name: Install pnpm |
| 21 | uses: pnpm/action-setup@v5 |
| 22 | |
| 23 | - name: Setup Node.js |
| 24 | uses: actions/setup-node@v6 |
RemediationAI
The problem is that `.github/workflows/guard-fixtures.yml` uses `actions/checkout@v6`, `pnpm/action-setup@v5`, and `actions/setup-node@v6` which are mutable tags that can be rewritten by a compromised maintainer, allowing silent injection of malicious code into the CI pipeline. Replace these with pinned 40-character commit SHAs, e.g., `actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v6.1.0`, `pnpm/action-setup@c3ca66f6d009969d338f8301cb08112290e51338 # v5`, and `actions/setup-node@60edb5dd545a775178fbb8726d51985e50cf05b2 # v6`, using tools like `pinact` or `ratchet` to automate the process. This fix ensures that the exact versions of the actions are immutable and cannot be silently replaced without explicit code review. Verify by running `git log --oneline` on each action's repository to confirm the SHAs correspond to the intended version tags.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 14 | steps: |
| 15 | - uses: actions/checkout@v6 |
| 16 | - uses: pnpm/action-setup@v5 |
| 17 | - uses: actions/setup-node@v6 |
| 18 | with: |
| 19 | node-version: 24 |
| 20 | cache: pnpm |
RemediationAI
The problem is that `.github/workflows/guard-benchmarks.yml` uses `actions/checkout@v6`, `pnpm/action-setup@v5`, and `actions/setup-node@v6` which are mutable tags that can be rewritten by a compromised maintainer, allowing silent injection of malicious code into the CI pipeline. Replace these with pinned 40-character commit SHAs, e.g., `actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v6.1.0`, `pnpm/action-setup@c3ca66f6d009969d338f8301cb08112290e51338 # v5`, and `actions/setup-node@60edb5dd545a775178fbb8726d51985e50cf05b2 # v6`, using tools like `pinact` or `ratchet` to automate the process. This fix ensures that the exact versions of the actions are immutable and cannot be silently replaced without explicit code review. Verify by running `git log --oneline` on each action's repository to confirm the SHAs correspond to the intended version tags.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 33 | uses: pnpm/action-setup@v5 |
| 34 | |
| 35 | - name: Setup Node.js |
| 36 | uses: actions/setup-node@v6 |
| 37 | with: |
| 38 | node-version: 24 |
| 39 | cache: pnpm |
RemediationAI
The problem is that `.github/workflows/release.yml` uses `pnpm/action-setup@v5` and `actions/setup-node@v6` which are mutable tags that can be rewritten by a compromised maintainer, allowing silent injection of malicious code into the CI pipeline. Replace these with pinned 40-character commit SHAs, e.g., `pnpm/action-setup@c3ca66f6d009969d338f8301cb08112290e51338 # v5` and `actions/setup-node@60edb5dd545a775178fbb8726d51985e50cf05b2 # v6`, using tools like `pinact` or `ratchet` to automate the process. This fix ensures that the exact versions of the actions are immutable and cannot be silently replaced without explicit code review. Verify by running `git log --oneline` on each action's repository to confirm the SHAs correspond to the intended version tags.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 25 | if: github.event_name == 'workflow_dispatch' || (startsWith(github.event.release.tag_name, 'v') && github.event.release.prerelease == false) |
| 26 | steps: |
| 27 | - name: Checkout |
| 28 | uses: actions/checkout@v6 |
| 29 | with: |
| 30 | ref: ${{ inputs.tag || github.event.release.tag_name }} |
RemediationAI
The problem is that `.github/workflows/release.yml` uses `actions/checkout@v6` which is a mutable tag that can be rewritten by a compromised maintainer, allowing silent injection of malicious code into the CI pipeline. Replace `actions/checkout@v6` with a pinned 40-character commit SHA, e.g., `actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v6.1.0`, using tools like `pinact` or `ratchet` to automate the process. This fix ensures that the exact version of the action is immutable and cannot be silently replaced without explicit code review. Verify by running `git log --oneline actions/checkout` to confirm the SHA corresponds to the intended version tag.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 21 | uses: actions/checkout@v6 |
| 22 | |
| 23 | - name: Install pnpm |
| 24 | uses: pnpm/action-setup@v5 |
| 25 | |
| 26 | - name: Setup Node.js |
| 27 | uses: actions/setup-node@v6 |
RemediationAI
The problem is that `.github/workflows/ci.yml` uses `actions/checkout@v6` which is a mutable tag that can be rewritten by a compromised maintainer, allowing silent injection of malicious code into the CI pipeline. Replace `actions/checkout@v6` with a pinned 40-character commit SHA, e.g., `actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v6.1.0`, using tools like `pinact` or `ratchet` to automate the process. This fix ensures that the exact version of the action is immutable and cannot be silently replaced without explicit code review. Verify by running `git log --oneline actions/checkout` to confirm the SHA corresponds to the intended version tag.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 12 | benchmark: |
| 13 | runs-on: ubuntu-latest |
| 14 | steps: |
| 15 | - uses: actions/checkout@v6 |
| 16 | - uses: pnpm/action-setup@v5 |
| 17 | - uses: actions/setup-node@v6 |
| 18 | with: |
RemediationAI
The problem is that `.github/workflows/guard-benchmarks.yml` uses `actions/checkout@v6`, `pnpm/action-setup@v5`, and `actions/setup-node@v6` which are mutable tags that can be rewritten by a compromised maintainer, allowing silent injection of malicious code into the CI pipeline. Replace these with pinned 40-character commit SHAs, e.g., `actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v6.1.0`, `pnpm/action-setup@c3ca66f6d009969d338f8301cb08112290e51338 # v5`, and `actions/setup-node@60edb5dd545a775178fbb8726d51985e50cf05b2 # v6`, using tools like `pinact` or `ratchet` to automate the process. This fix ensures that the exact versions of the actions are immutable and cannot be silently replaced without explicit code review. Verify by running `git log --oneline` on each action's repository to confirm the SHAs correspond to the intended version tags.
Time-of-check-to-time-of-use race. Code calls `os.path.exists` / `fs.existsSync` to check a path, then `open` / `readFileSync` / `unlink` on the same name within a few lines โ without a lock or atomic-open. An attacker who can race the filesystem (symlink, file replacement) between the check and the use gets the action applied to a different target. Replace the check-then-use pattern with the action's own error handling: try the open and catch FileNotFoundError / ENOENT. For atomic creation use
Evidence
| 1 | import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; |
| 2 | import { spawn } from 'node:child_process'; |
| 3 | import { tmpdir } from 'node:os'; |
| 4 | import { join, resolve } from 'node:path'; |
| 5 | |
| 6 | const cli = resolve(process.env.GUARD_CLI ?? 'dist/cli/index.js'); |
| 7 | const writers = Number(process.env.GUARD_CONCURRENT_WRITERS ?? 8); |
| 8 | if (!Number.isInteger(writers) || writers < 2) |
| 9 | throw new Error('GUARD_CONCURRENT_WRITERS must be at least 2.'); |
| 10 | |
| 11 | const root = mkdtempSync(join( |
RemediationAI
The problem is that `scripts/guard-concurrent-state.mjs` calls `existsSync()` to check if a file exists, then calls `readFileSync()` or `rmSync()` on the same path without a lock, creating a race condition where an attacker can replace the file with a symlink between the check and the use. Replace the check-then-use pattern by removing the `existsSync()` call and wrapping the file operation in a try-catch block that handles the `ENOENT` error, e.g., `try { const data = readFileSync(path); } catch (err) { if (err.code !== 'ENOENT') throw err; }`. This fix eliminates the race window by making the operation atomic and relying on the filesystem's own error handling. Verify by running the script under concurrent load (e.g., with `guard-concurrent-state.mjs` in a loop while another process modifies the same files) and confirm no unexpected file operations occur.
Time-of-check-to-time-of-use race. Code calls `os.path.exists` / `fs.existsSync` to check a path, then `open` / `readFileSync` / `unlink` on the same name within a few lines โ without a lock or atomic-open. An attacker who can race the filesystem (symlink, file replacement) between the check and the use gets the action applied to a different target. Replace the check-then-use pattern with the action's own error handling: try the open and catch FileNotFoundError / ENOENT. For atomic creation use
Evidence
| 1 | import { execFileSync } from 'node:child_process'; |
| 2 | import { createHash, randomUUID } from 'node:crypto'; |
| 3 | import { hostname } from 'node:os'; |
| 4 | import { |
| 5 | existsSync, |
| 6 | lstatSync, |
| 7 | mkdirSync, |
| 8 | readdirSync, |
| 9 | readFileSync, |
| 10 | realpathSync, |
| 11 | renameSync, |
| 12 | statSync, |
| 13 | unlinkSync, |
| 14 | writeFileSync, |
| 15 | } from 'node:fs'; |
| 16 | import { dirname, extname, relative, resolve, sep } from 'node:path'; |
| 17 | import { Project, SyntaxKind, type SourceFile } from 'ts-morph'; |
| 18 | import { buildModuleTargetGraph, type ProjectModel } |
Remediation
Replace check-then-use with action-then-handle: Python: `try: with open(p) as f: ... except FileNotFoundError: ...` Node: `try { fs.readFileSync(p); } catch (e) { if (e.code === "ENOENT") ... }` For atomic file creation: Python: `os.open(p, os.O_RDWR | os.O_CREAT | os.O_EXCL)` Node: `fs.open(p, "wx")` โ fails if file exists, no race. When you genuinely must check first, use `flock` (Python `fcntl`) or a similar per-process advisory lock to make the window uninteresting to a
Time-of-check-to-time-of-use race. Code calls `os.path.exists` / `fs.existsSync` to check a path, then `open` / `readFileSync` / `unlink` on the same name within a few lines โ without a lock or atomic-open. An attacker who can race the filesystem (symlink, file replacement) between the check and the use gets the action applied to a different target. Replace the check-then-use pattern with the action's own error handling: try the open and catch FileNotFoundError / ENOENT. For atomic creation use
Evidence
| 1 | import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; |
| 2 | import { dirname, resolve } from 'node:path'; |
| 3 | |
| 4 | export type GuardAgentTarget = 'generic' | 'codex' | 'cursor' | 'claude' | 'copilot' | 'gemini'; |
| 5 | |
| 6 | export interface GuardAgentInstallResult { |
| 7 | target: GuardAgentTarget; |
| 8 | path: string; |
| 9 | action: 'created' | 'updated'; |
| 10 | } |
| 11 | |
| 12 | export class GuardAgentInstallError extends Error { |
| 13 | constructor(message: string) { |
| 14 | super(message); |
| 15 | this.name = 'GuardAgentInstallErro |
Remediation
Replace check-then-use with action-then-handle: Python: `try: with open(p) as f: ... except FileNotFoundError: ...` Node: `try { fs.readFileSync(p); } catch (e) { if (e.code === "ENOENT") ... }` For atomic file creation: Python: `os.open(p, os.O_RDWR | os.O_CREAT | os.O_EXCL)` Node: `fs.open(p, "wx")` โ fails if file exists, no race. When you genuinely must check first, use `flock` (Python `fcntl`) or a similar per-process advisory lock to make the window uninteresting to a