Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
typecheck:
runs-on: ubuntu-latest
Comment on lines +9 to +11

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”’ Security & Privacy | 🟠 Major | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow files =="
git ls-files .github/workflows || true

echo "== ci.yml outline =="
wc -l .github/workflows/ci.yml 2>/dev/null || true
cat -n .github/workflows/ci.yml 2>/dev/null | sed -n '1,220p'

echo "== permissions in workflows =="
rg -n '^\s*permissions:|^\s*pull_request:|uses:|run:|chmod|git (push|commit|create)' .github/workflows || true

echo "== repo default permissions references =="
rg -n 'GITHUB_TOKEN|contents:|pull_request_target|pull_request:' -S .github/workflows . 2>/dev/null | head -200 || true

Repository: leeknowsai/paperclip-plugin-superpowers

Length of output: 2117


Declare read-only GITHUB_TOKEN permissions.

Add a top-level permissions block before jobs because this workflow lacks an explicit permission grant. Without it, repository or organization defaults can grant write access, even though this workflow only needs contents read access for checkout and reviewable repository code.

Proposed fix
 on:
   push:
     branches: [main]
   pull_request:
     branches: [main]
 
+permissions:
+  contents: read
+
 jobs:
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 9 - 11, Add a top-level permissions
block before jobs in the workflow, granting only read access to repository
contents via contents: read; leave the existing typecheck job unchanged.

Source: Linters/SAST tools

steps:
- uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”’ Security & Privacy | 🟑 Minor | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow files =="
git ls-files .github/workflows || true

echo
echo "== ci.yml =="
if [ -f .github/workflows/ci.yml ]; then
  cat -n .github/workflows/ci.yml | sed -n '1,80p'
fi

echo
echo "== package build script =="
if [ -f package.json ]; then
  node -e "const p=require('./package.json'); console.log(JSON.stringify(p.scripts, null, 2))" 2>/dev/null || cat -n package.json | sed -n '1,40p'
fi

echo
echo "== build-ui script =="
for f in scripts/build-ui.mjs scripts/build-ui.js scripts/build-ui.ts; do
  if [ -f "$f" ]; then
    echo "-- $f --"
    cat -n "$f" | sed -n '1,220p'
  fi
done

echo
echo "== checkout occurrences =="
rg -n "actions/checkout|persist-credentials|pnpm build|build-ui|GITHUB_TOKEN|process\.env\." .github package.json scripts 2>/dev/null || true

Repository: leeknowsai/paperclip-plugin-superpowers

Length of output: 2433


🌐 Web query:

actions/checkout v4 README persist-credentials default true

πŸ’‘ Result:

In the actions/checkout v4 action, the persist-credentials input defaults to true [1][2]. When set to true (the default), the action configures the authentication token or SSH key into the local git configuration [1][2]. This allows your workflow scripts to run authenticated git commands, such as git push or git pull, during the job execution [1][3]. The credentials are removed automatically during the post-job cleanup process [1][3]. If you do not want these credentials to be persisted in the local git configuration, you can explicitly set persist-credentials to false [1][4].

Citations:


🌐 Web query:

GitHub Actions GITHUB_TOKEN environment variable persist-credentials local git config

πŸ’‘ Result:

In GitHub Actions, the persist-credentials input for the actions/checkout action controls whether the repository's authentication token (typically GITHUB_TOKEN) is configured for use by local git commands [1][2]. When persist-credentials is set to true (the default), actions/checkout configures the authentication credentials to allow subsequent steps in the job to run authenticated git commands like git fetch or git push without needing to manually re-authenticate [1][3]. Key details regarding this behavior include: 1. Credential Location: In older versions of actions/checkout (v5 and earlier), these credentials were stored directly in the local .git/config file [4][5]. Starting with actions/checkout v6, the action was updated to store these credentials in a separate file located in $RUNNER_TEMP instead of directly modifying .git/config, which improves security [3][5][6]. 2. Purpose: This persistence is what allows scripts within your workflow to execute authenticated git operations seamlessly using the environment's existing identity [1][3]. 3. Disabling Persistence: If your workflow does not require running authenticated git commands, or if you want to prevent the credentials from being available to subsequent steps, you can set persist-credentials: false in your workflow step [1][7][4]. 4. Security Considerations: Because these credentials allow authenticated git operations, they are sensitive. Historically, concerns were raised that persisting them in .git/config could lead to accidental leakage (e.g., if the workspace was uploaded as an artifact) [4]. While v6+ mitigates this by moving the storage location, security best practices still recommend setting persist-credentials: false if the functionality is not explicitly required, as well as applying the principle of least privilege to the GITHUB_TOKEN permissions [8][9][4]. Note that this configuration can also cause conflicts if you are using tools like gh auth setup-git to configure different credentials, as the existing actions/checkout configuration may take precedence [7].

Citations:


🌐 Web query:

actions/checkout github app token persist-credentials local git config v4

πŸ’‘ Result:

In actions/checkout@v4, the persist-credentials input controls whether the authentication token (or SSH key) used to fetch the repository is configured in the local Git configuration [1][2]. Key details regarding this setting include: - Default Behavior: The input defaults to true [2][3]. When enabled, the action configures the Git credential (typically the GITHUB_TOKEN or a provided PAT/SSH key) into the local repository's .git/config file [2][4]. This allows subsequent steps in the same workflow job to perform authenticated Git operations (e.g., git push, git fetch) without needing to explicitly provide credentials again [1][5]. - Security Implications: Because the credentials are written to the .git/config file, they may be exposed if the workspace (including the .git directory) is handled by other processes or included in artifacts that are uploaded or shared [4]. - Opting Out: You can set persist-credentials: false to prevent the action from writing these credentials to the local Git configuration [1][3]. This is recommended as a security best practice if downstream steps do not require authenticated Git access, as it reduces the attack surface by ensuring no long-lived credentials remain in the environment's Git config [4]. - Cleanup: In actions/checkout@v4, the action includes a post-job step designed to remove these persisted credentials from the Git configuration after the job completes [1][2]. For environments requiring higher security or where credentials must not be stored on disk, persist-credentials: false is the standard configuration to ensure the runner environment remains isolated from the repository's access tokens [4][6].

Citations:


Disable checkout credential persistence.

Set persist-credentials: false on both checkout steps. actions/checkout@v4 persists the Git credentials by default, and this workflow does not need authenticated Git commands later.

Proposed fix
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v4
+        with:
+          persist-credentials: false

The same change applies to the checkout step in the build job and the checkout step in the typecheck job.

πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
persist-credentials: false
🧰 Tools
πŸͺ› zizmor (1.28.0)

[warning] 13-13: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml at line 13, Update both actions/checkout@v4 steps
in the build and typecheck jobs to set persist-credentials to false, ensuring
neither checkout retains Git credentials for later workflow steps.

Source: Linters/SAST tools

- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build
38 changes: 30 additions & 8 deletions src/sync/github-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,36 +102,58 @@ export class GitHubSync {
return { synced, errors, total: synced + errors };
}

/** Build auth headers from GITHUB_TOKEN env var (optional). */
private authHeaders(): Record<string, string> {
const token = process.env.GITHUB_TOKEN;
const headers: Record<string, string> = {
Accept: "application/vnd.github+json",
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
return headers;
}

private async fetchTree(): Promise<TreeEntry[]> {
const url = `https://api.github.com/repos/${this.repo}/git/trees/main?recursive=1`;
const res = await fetch(url, {
headers: { Accept: "application/vnd.github+json" },
});
const res = await fetch(url, { headers: this.authHeaders() });
if (!res.ok) {
throw new Error(`GitHub tree API returned ${res.status}`);
const detail = await res.text().catch(() => "");
throw new Error(`GitHub tree API returned ${res.status}: ${detail.slice(0, 200)}`);
Comment on lines +121 to +122

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n 'res\.text\(\)|detail\.slice\(0,\s*200\)' src/sync/github-sync.ts

node --input-type=module <<'EOF'
const size = 1024 * 1024;
const response = new Response("x".repeat(size), { status: 500 });
const body = await response.text();

if (body.length !== size) {
  throw new Error("Response.text() did not consume the full body");
}
console.log("Response.text() consumes the full response body");
EOF

Repository: leeknowsai/paperclip-plugin-superpowers

Length of output: 722


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf 'Outline:\n'
ast-grep outline src/sync/github-sync.ts --view compact || true

printf '\nRelevant source:\n'
sed -n '1,170p' src/sync/github-sync.ts

Repository: leeknowsai/paperclip-plugin-superpowers

Length of output: 5916


Bound the error-body read before truncating it.

fetchTree() and fetchRawContent() call res.text() before using only the first 200 characters of the body. Use a bounded helper that reads at most 200 bytes from res.body, cancels the stream reader, and uses it for both non-2xx body reads.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/sync/github-sync.ts` around lines 121 - 122, Update fetchTree() and
fetchRawContent() to use a shared bounded response-body helper that reads at
most 200 bytes from res.body, cancels the stream reader, and returns the
truncated text. Replace both direct res.text() calls in the non-2xx error paths
while preserving the existing status-specific error messages.

}
const data = (await res.json()) as { tree: TreeEntry[] };
return data.tree;
}

private async fetchRawContent(path: string): Promise<string> {
const url = `https://raw.githubusercontent.com/${this.repo}/main/${path}`;
const res = await fetch(url);
const res = await fetch(url, { headers: this.authHeaders() });
if (!res.ok) {
throw new Error(`Failed to fetch ${path}: ${res.status}`);
const detail = await res.text().catch(() => "");
throw new Error(`Failed to fetch ${path}: ${res.status} β€” ${detail.slice(0, 200)}`);
}
return res.text();
}

parseSkillFile(raw: string, skillId: string): Skill | null {
// Extract YAML frontmatter between --- markers
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!fmMatch) return null;
if (!fmMatch) {
this.ctx.logger.warn(
`Skill "${skillId}" has no valid YAML frontmatter (expected ---\nname: ...\n---). Skipping.`,
);
return null;
}

const frontmatter = fmMatch[1];
const body = fmMatch[2].trim();
const fm = this.parseSimpleYaml(frontmatter);
if (!fm.name) return null;
if (!fm.name) {
this.ctx.logger.warn(
`Skill "${skillId}" frontmatter is missing required "name" field. Skipping.`,
);
return null;
}

return {
id: skillId,
Expand Down