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
162 changes: 162 additions & 0 deletions .agents/skills/knip/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
---
name: knip
description:
Run knip to find and remove unused files, dependencies, and exports. Use for cleaning up dead code and unused
dependencies.
---

# Knip Code Cleanup

Run knip to find and remove unused files, dependencies, and exports from this codebase.

## Setup

1. Check if knip is available:
- Run `npx knip --version` to test
- If it fails or is very slow, check if `knip` is in package.json devDependencies
- If not installed locally, install with `npm install -D knip` (or pnpm/yarn/bun equivalent based on lockfile
present)
Comment on lines +14 to +18

Copy link
Copy Markdown
Contributor

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

printf 'FILE LIST (skill/doc related)\n'
git ls-files '.agents/skills/knip/SKILL.md' 'package.json' 'pnpm-lock.yaml' 'yarn.lock' 'package-lock.json' 'bun.lockb' | sed 's#^`#-` #'

printf '\nSKILL FILE SNIPPET\n'
cat -n .agents/skills/knip/SKILL.md | sed -n '1,220p'

printf '\nPACKAGE SCRIPTS / KNIP REFERENCES\n'
python3 - <<'PY'
import json, pathlib, sys
p = pathlib.Path('package.json')
if p.exists():
    data = json.loads(p.read_text())
    print('package.json scripts:')
    for k,v in (data.get('scripts') or {}).items():
        if 'knip' in k.lower() or 'knip' in str(v).lower():
            print(f'  {k}: {v}')
    print('\npackage.json knip refs:')
    for field in ['devDependencies','dependencies','optionalDependencies','peerDependencies']:
        deps = data.get(field) or {}
        if 'knip' in deps:
            print(f'  {field}.knip: {deps["knip"]}')
else:
    print('no package.json')
PY

printf '\nSEARCH KNIP REFERENCES\n'
rg -n --hidden --glob '!**/.git/**' '\bknip\b|npx knip|npm run knip' .agents package.json . 2>/dev/null | sed -n '1,220p'

Repository: netlify/build

Length of output: 14071


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'FILE LIST (skill/doc related)\n'
git ls-files '.agents/skills/knip/SKILL.md' 'package.json' 'pnpm-lock.yaml' 'yarn.lock' 'package-lock.json' 'bun.lockb' | sed 's#^`#-` #'

printf '\nSKILL FILE SNIPPET\n'
cat -n .agents/skills/knip/SKILL.md | sed -n '1,220p'

printf '\nPACKAGE SCRIPTS / KNIP REFERENCES\n'
python3 - <<'PY'
import json, pathlib
p = pathlib.Path('package.json')
if p.exists():
    data = json.loads(p.read_text())
    print('package.json scripts:')
    for k,v in (data.get('scripts') or {}).items():
        if 'knip' in k.lower() or 'knip' in str(v).lower():
            print(f'  {k}: {v}')
    print('\npackage.json knip refs:')
    for field in ['devDependencies','dependencies','optionalDependencies','peerDependencies']:
        deps = data.get(field) or {}
        if 'knip' in deps:
            print(f'  {field}.knip: {deps["knip"]}')
else:
    print('no package.json')
PY

printf '\nSEARCH KNIP REFERENCES\n'
rg -n --hidden --glob '!**/.git/**' '\bknip\b|npx knip|npm run knip' .agents package.json . 2>/dev/null | sed -n '1,220p'

Repository: netlify/build

Length of output: 14071


Use the repo-local knip script instead of bare npx

.agents/skills/knip/SKILL.md already has knip pinned in devDependencies and exposed as an npm script, so the setup and command examples should stay on the lockfile-backed path (npm run knip -- ...) and avoid npx knip --version as the availability check.

🧰 Tools
🪛 SkillSpector (2.3.7)

[warning] 15: [RP1] null: npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Remediation: Pin the version: npx @scope/server@1.2.3

(MCP Rug Pull (RP1))


[warning] 37: [RP1] null: npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Remediation: Pin the version: npx @scope/server@1.2.3

(MCP Rug Pull (RP1))


[warning] 92: [RP1] null: npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Remediation: Pin the version: npx @scope/server@1.2.3

(MCP Rug Pull (RP1))


[warning] 122: [RP1] null: npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Remediation: Pin the version: npx @scope/server@1.2.3

(MCP Rug Pull (RP1))


[warning] 125: [RP1] null: npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Remediation: Pin the version: npx @scope/server@1.2.3

(MCP Rug Pull (RP1))


[warning] 143: [RP1] null: npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Remediation: Pin the version: npx @scope/server@1.2.3

(MCP Rug Pull (RP1))


[warning] 146: [RP1] null: npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Remediation: Pin the version: npx @scope/server@1.2.3

(MCP Rug Pull (RP1))


[warning] 149: [RP1] null: npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Remediation: Pin the version: npx @scope/server@1.2.3

(MCP Rug Pull (RP1))


[warning] 152: [RP1] null: npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Remediation: Pin the version: npx @scope/server@1.2.3

(MCP Rug Pull (RP1))


[warning] 155: [RP1] null: npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Remediation: Pin the version: npx @scope/server@1.2.3

(MCP Rug Pull (RP1))

🤖 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 @.agents/skills/knip/SKILL.md around lines 14 - 18, The knip skill guide is
using a bare npx-based availability check even though the repo already provides
a lockfile-backed knip dependency and npm script. Update the setup and command
examples in SKILL.md to use the local script path via npm run knip -- ... (or
the repo’s equivalent script), and remove the npx knip --version check so the
documented workflow stays pinned to the repo-local installation.

Source: Linters/SAST tools


2. Knip does NOT remove unused imports/variables inside files — that's a linter's job. Knip finds unused files,
dependencies, and exports across the project.

## Workflow

Always follow this configuration-first workflow. Even for simple "run knip" or "clean up codebase" prompts, configure
knip properly before acting on reported issues.

### Step 1: Understand the project

- Check what frameworks and tools the project uses (look at package.json)
- Check if a knip config exists (`knip.json`, `knip.jsonc`, or `knip` key in package.json)
- If a config exists, review it for improvements (see Configuration Best Practices below)

### Step 2: Run knip and read configuration hints first

```bash
npx knip
```

Focus on **configuration hints** before anything else. These appear at the top of the output and suggest config
adjustments to reduce false positives.

### Step 3: Address hints by adjusting knip.json

Fix configuration hints before addressing reported issues. Common adjustments:

- Enable/disable plugins for detected frameworks
- Add entry patterns for non-standard entry points
- Configure workspace settings for monorepos

### Step 4: Repeat steps 2-3

Re-run knip after each config change. Repeat until configuration hints are resolved and false positives are minimized.

### Step 5: Address actual issues

Once the configuration is settled, work through reported issues. Prioritize in this order:

1. **Unused files** — address these first ("inbox zero" approach removes the most noise)
2. **Unused dependencies** — remove from package.json
3. **Unused devDependencies** — remove from package.json
4. **Unused exports** — remove or mark as internal
5. **Unused types** — remove, or configure `ignoreExportsUsedInFile` (see below)

### Step 6: Re-run and repeat

Re-run knip after each batch of fixes. Removing unused files often exposes newly-unused exports and dependencies.

## Configuration Best Practices

When reviewing or creating a knip config, follow these rules:

- **Never use `ignore` patterns** — `ignore` hides real issues and should almost never be used. Always prefer specific
solutions. Other `ignore*` options (like `ignoreDependencies`, `ignoreExportsUsedInFile`) are fine because they target
specific issue types.
- **Many unused exported types?** Add `ignoreExportsUsedInFile: { interface: true, type: true }` — this handles the
common case of types only used in the same file. Prefer this over broader ignore options.
- **Remove redundant patterns** — Knip already respects `.gitignore`, so ignoring `node_modules`, `dist`, `build`,
`.git` is redundant.
- **Remove entry patterns covered by defaults** — Auto-detected plugins already add standard entry points. Don't
duplicate them.
- **Config files showing as unused** (e.g. `vite.config.ts`) — Enable or disable the corresponding plugin explicitly
rather than ignoring the file.
- **Dependencies matching Node.js builtins** (e.g. `buffer`, `process`) — Add to `ignoreDependencies`.
- **Unresolved imports from path aliases** — Add `paths` to knip config (uses tsconfig.json semantics).

## Production Mode

Use `--production` to focus on production code only:

```bash
npx knip --production
```

This excludes test files, config files, and other non-production entry points. Do NOT use `project` or `ignore` patterns
to exclude test files — use `--production` instead.

## Cleanup Confidence Levels

### Auto-delete (high confidence):

- Unused exports that are clearly internal (not part of public API)
- Unused type exports
- Unused dependencies (remove from package.json)
- Unused files that are clearly orphaned (not entry points, not config files)

### Ask first (needs clarification):

- Files that might be entry points or dynamically imported
- Exports that might be part of a public API (index.ts, lib exports)
- Dependencies that might be used via CLI or peer dependencies
- Anything in paths like `src/index`, `lib/`, or files with "public" or "api" in the name

Use the AskUserQuestion tool to clarify before deleting these.

## Auto-fix

Once configuration is settled and you're confident in the results:

```bash
# Auto-fix safe changes (removes unused exports and dependencies)
npx knip --fix

# Auto-fix including file deletion
npx knip --fix --allow-remove-files
```

Only use `--fix` after the configuration-first workflow is complete.

## Error Handling

If knip exits with code 2 (unexpected error like "error loading file"):

- Check if a config file exists — if not, create `knip.json` in the project root
- Check for known issues at knip.dev
- Review the configuration reference for syntax/option errors
- Run knip again after fixes

## Common Commands

```bash
# Basic run
npx knip

# Production only (excludes test/config entry points)
npx knip --production

# Auto-fix what's safe
npx knip --fix

# Auto-fix including file deletion
npx knip --fix --allow-remove-files

# JSON output for parsing
npx knip --reporter json
```

## Notes

- Watch for monorepo setups — may need `--workspace` flag
- Some frameworks need plugins enabled in config
- Knip does not handle unused imports/variables inside files — use ESLint or Biome for that
1 change: 1 addition & 0 deletions .claude/skills/knip
2 changes: 1 addition & 1 deletion .github/workflows/conventional-commit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ jobs:
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install Dependencies
run: npm install @commitlint/config-conventional
run: npm ci
Comment on lines 10 to +12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== conventional-commit workflow ==\n'
sed -n '1,120p' .github/workflows/conventional-commit.yml

printf '\n== workflows mentioning setup-node ==\n'
rg -n "actions/setup-node|node-version|npm ci|cache: 'npm'|cache: npm" .github/workflows -S || true

printf '\n== package manager / engines clues ==\n'
rg -n "\"packageManager\"|\"engines\"|node-version|volta|nvm|asdf" package.json .nvmrc .node-version .tool-versions .github/workflows -S || true

Repository: netlify/build

Length of output: 4824


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== workflow files ==\n'
find .github/workflows -maxdepth 1 -type f | sort

printf '\n== relevant package metadata ==\n'
for f in package.json .nvmrc .node-version .tool-versions; do
  if [ -f "$f" ]; then
    echo "--- $f"
    sed -n '1,120p' "$f"
  fi
done

Repository: netlify/build

Length of output: 2500


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== workflow runtime references in repository ==\n'
rg -n "ubuntu-latest|setup-node@|node-version:|npm ci|npm install|corepack|pnpm|yarn" .github/workflows -S || true

Repository: netlify/build

Length of output: 3725


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Focus on the reviewed workflow and any repo-standard Node setup in CI.
printf '\n== conventional-commit workflow ==\n'
cat -n .github/workflows/conventional-commit.yml

printf '\n== any workflow using setup-node ==\n'
rg -n "actions/setup-node" .github/workflows -S || true

printf '\n== repo node version markers ==\n'
for f in package.json .nvmrc .node-version .tool-versions; do
  [ -f "$f" ] && { echo "--- $f"; cat "$f"; }
done

Repository: netlify/build

Length of output: 3431


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== conventional-commit.yml =='
sed -n '1,80p' .github/workflows/conventional-commit.yml

echo
echo '== setup-node occurrences =='
rg -n "actions/setup-node|node-version|cache: 'npm'|cache: npm" .github/workflows -S || true

echo
echo '== Node version markers =='
for f in package.json .nvmrc .node-version .tool-versions; do
  if [ -f "$f" ]; then
    echo "--- $f"
    sed -n '1,60p' "$f"
  fi
done

Repository: netlify/build

Length of output: 4143


Pin Node before npm ci here. This job depends on the runner’s preinstalled Node/npm; add actions/setup-node first to keep the check reproducible and match the other workflows in this repo.

🧰 Tools
🪛 zizmor (1.26.1)

[warning] 10-10: 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/conventional-commit.yml around lines 10 - 12, The
conventional-commit workflow currently runs npm ci before ensuring a specific
Node version is installed, so the job depends on whatever Node/npm is
preinstalled on the runner. Update the job to add actions/setup-node before the
Install Dependencies step, using the same Node version/pinning approach as the
other workflows in this repo, and keep the existing actions/checkout step
followed by setup-node and then npm ci.

- uses: JulienKode/pull-request-name-linter-action@4fb4c2773193ad7ae5fe105c98ab30778abb1536 # v20.1.0
27 changes: 27 additions & 0 deletions .github/workflows/knip.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: 🧹 Knip

on:
pull_request:
types: [opened, synchronize, reopened]
permissions: {}

jobs:
knip:
runs-on: ubuntu-24.04-arm
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
Comment on lines +14 to +16

Copy link
Copy Markdown
Contributor

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

printf '\n== workflow file ==\n'
cat -n .github/workflows/knip.yml

printf '\n== locate pull_request workflow + checkout usage ==\n'
rg -n --context 3 'pull_request|actions/checkout|persist-credentials|npm ci|npm run build|npm run knip' .github/workflows/knip.yml

Repository: netlify/build

Length of output: 1644


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== actions/checkout default docs in repo (if any) ==\n'
rg -n --context 2 'persist-credentials' .github README.md docs . 2>/dev/null || true

Repository: netlify/build

Length of output: 205


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat -n .github/workflows/knip.yml
printf '\n---\n'
rg -n --context 3 'pull_request|actions/checkout|persist-credentials|npm ci|npm run build|npm run knip' .github/workflows/knip.yml

Repository: netlify/build

Length of output: 1575


🌐 Web query:

actions/checkout persist-credentials default true documentation

💡 Result:

The persist-credentials input for the actions/checkout GitHub Action has a default value of true [1][2][3]. When set to true, this input configures the GitHub Actions authentication token (or SSH key) into the local git configuration [1][4]. This allows your workflow scripts to run authenticated git commands (such as git push or git fetch) directly within the workspace [1][5]. The action automatically removes these credentials during the post-job cleanup phase [1][2]. For improved security, modern versions of the action store these credentials in a separate file under $RUNNER_TEMP rather than directly within the.git/config file, which helps prevent them from being inadvertently included in artifacts [1][2]. If you wish to disable this behavior for security reasons or if it is not required for your workflow, you can explicitly set the input to false in your workflow file [1][5]: - uses: actions/checkout@v4 with: persist-credentials: false

Citations:


Disable checkout credential persistence in the Knip workflow.

This job runs repo-controlled install/build steps on pull_request, so leaving actions/checkout at its default credential persistence keeps the token available in local git config. Set persist-credentials: false.

Suggested fix
       - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
         with:
           fetch-depth: 0
+          persist-credentials: false
📝 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 14-16: 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/knip.yml around lines 14 - 16, The Knip workflow’s
checkout step is leaving credentials persisted by default, which keeps the
GitHub token in local git config during pull_request builds. Update the existing
actions/checkout step in the knip job to disable credential persistence by
setting persist-credentials to false. Keep the change scoped to the checkout
configuration so the rest of the workflow behavior stays the same.

Source: Linters/SAST tools

- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '24'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build packages
run: npm run build
- name: Find unused code
run: npm run knip
66 changes: 66 additions & 0 deletions knip.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"$schema": "https://unpkg.com/knip@6/schema.json",
"ignore": ["**/fixtures/**", "**/vendor/**"],
// Don't flag extraneous `export`s, low impact not worth the noise.
"ignoreExportsUsedInFile": true,
// Used magically by `conventional-commit.yml` GHA workflow
// TODO(serhalp): Lint PR titles a more explicit way and remove this
"commitlint": {
"config": ["commitlint.config.js"],
},
"workspaces": {
".": {},
"packages/*": {},
"packages/build": {
"entry": ["test-d/**/*.ts"],
"ignoreDependencies": [
// CLI binary spawned as a subprocess (not imported), so knip can't trace it.
"atob",
// Imported only from a test fixture, which the global `**/fixtures/**` ignore hides.
"process-exists",
// CLI binary spawned as a subprocess (not imported), so knip can't trace it.
"yarn",
// Used in tests but can't be declared as a dependency due to a circular dependency.
"@netlify/testing",
// Intentionally an optional peer dep, resolved defensively at runtime
"@netlify/opentelemetry-sdk-setup",
],
},
"packages/config": {
"ignoreDependencies": [
// Used in tests but can't be declared as a dependency due to a circular dependency.
"@netlify/testing",
],
},
"packages/build-info": {
"entry": [
"e2e/**/*.e2e.ts",
// knip doesn't pick up the playwright -> npm -> vite -> <script> entry
"e2e/fixture/main.ts",
],
},
"packages/edge-bundler": {
// knip doesn't pick up the Node.js -> dynamic `deno run <path>` entries
"entry": ["deno/bundle.ts", "deno/config.ts"],
"ignoreFiles": [
// TODO(serhalp): Probably move these into a `fixtures/` dir.
"test/integration/functions/func1.ts",
"test/integration/internal-functions/func2.ts",
"test/integration/test.js",
],
},
"packages/zip-it-and-ship-it": {
"entry": [
// knip doesn't pick up the npm -> bash -> node entry
"benchmarks/*.{js,ts}",
],
"ignore": ["**/fixtures-esm/**"],
"ignoreDependencies": [
// Imported only from a test fixture, which the global `**/fixtures/**` ignore hides.
"browserslist",
// Used from a test fixture via an intentionally obfuscated dynamic require knip can't resolve.
"cardinal",
],
},
},
}
Loading
Loading