skills/netlify-cli/SKILL.md
GitHub Actions workflows that deploy to Netlify with netlify-cli, or running 'netlify dev' / 'netlify functions:serve' locally. Covers monorepo 'Projects detected' errors, pnpm workspace deploys, deploying pre-built directories, capturing deploy URLs, GitHub secrets setup, netlify.toml inheritance with branch deploys, isolating sub-site deploys, netlify dev with --filter, pnpm 10.x trust store errors, CLI crash workarounds. Keywords: netlify deploy, github actions netlify, netlify-cli, monorepo deploy, pnpm workspace netlify, netlify dev, netlify functions serve.
npx skillsauth add takazudo/claude-resources netlify-cliInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill when writing or debugging GitHub Actions workflows that deploy to Netlify using netlify-cli. This skill contains critical knowledge about common pitfalls and solutions.
netlify deploy \
--dir=<path> # Directory to deploy (required)
--site=<site-id> # Netlify site ID
--auth=<token> # Auth token (or use NETLIFY_AUTH_TOKEN env var)
--prod # Deploy to production (default: draft)
--build # Opt-in to running build before deploy (deploy does NOT build by default)
--filter=<package> # Select package in monorepo - CRITICAL for pnpm workspaces
--functions=<folder> # Override functions directory (useful to skip functions with empty dir)
--alias=<name> # Custom subdomain for draft deploys
--message=<msg> # Deployment message
--json # Output as JSON (for programmatic URL extraction)
IMPORTANT: There is NO --no-build flag in netlify-cli@19+. netlify deploy does NOT build by default. Use --build only if you want to opt-in to building.
When deploying from a pnpm workspace or monorepo, you will encounter:
Error: Projects detected: package-a, package-b. Configure the project you want to work with and try again.
Use the --filter flag to select the target package:
netlify deploy \
--dir=./doc/build \
--site=$NETLIFY_SITE_ID \
--auth=$NETLIFY_AUTH_TOKEN \
--filter=<package-name> \
--prod
--filter=<package-name>: Selects the package (use the name from package.json)netlify deploy does NOT build by default, so no extra flag is needed to skip buildingThe CLI always reads netlify.toml from the project root, regardless of the --dir flag. This means:
netlify.toml apply to ALL deploys (including branch/alias deploys)netlify.toml has a redirect like /doc/* -> https://doc--mysite.netlify.app/doc/:splat with force = true, the branch deploy will proxy to itself, causing 404sTo prevent the root netlify.toml from affecting a sub-site deploy, deploy from a separate directory with its own minimal netlify.toml:
- name: Prepare deploy directory
run: |
mkdir -p deploy-output/doc
cp -r doc/build/* deploy-output/doc/
mkdir -p deploy-output/.empty-functions
cat > deploy-output/netlify.toml << 'TOML'
[build]
publish = "."
TOML
- name: Deploy to Netlify
working-directory: deploy-output
env:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
run: |
netlify deploy \
--dir=. \
--functions=.empty-functions \
--site=$NETLIFY_SITE_ID \
--auth=$NETLIFY_AUTH_TOKEN \
--alias=doc \
--message="Deploy: ${{ github.sha }}"
Key points:
working-directory: deploy-output makes the CLI find the local netlify.toml instead of the root one--functions=.empty-functions overrides the root's functions directory to avoid bundling errorsnetlify.toml has no redirects, so no circular proxy issues--filter is not needed when deploying from an isolated directory (no monorepo detection)When a sub-site is proxied from the main site under a path prefix (e.g., mysite.com/doc/ -> doc--mysite.netlify.app/doc/), the build output must be nested in a matching subdirectory.
Problem: netlify deploy --dir=doc/build deploys files at root /. But if the framework (e.g., Docusaurus with baseUrl: '/doc/') generates asset references like /doc/assets/main.js, they will 404 because the actual file is at /assets/main.js.
Solution: Wrap the build output in the correct subdirectory before deploying:
- name: Prepare deploy directory
run: |
mkdir -p deploy-output/doc
cp -r doc/build/* deploy-output/doc/
- name: Deploy
run: netlify deploy --dir=deploy-output ...
Now doc--mysite.netlify.app/doc/assets/main.js resolves correctly.
For non-monorepo projects:
- name: Install Netlify CLI
run: npm install -g netlify-cli
- name: Deploy to Netlify
run: |
netlify deploy \
--dir=./out \
--site=${{ secrets.NETLIFY_SITE_ID }} \
--auth=${{ secrets.NETLIFY_AUTH_TOKEN }} \
--prod \
--message="Deploy from GitHub Actions - ${{ github.sha }}"
- name: Deploy to Netlify
id: deploy
run: |
OUTPUT=$(netlify deploy \
--dir=./build \
--site=${{ secrets.NETLIFY_SITE_ID }} \
--auth=${{ secrets.NETLIFY_AUTH_TOKEN }} \
--prod \
--message="Deploy: ${{ github.sha }}" 2>&1)
echo "$OUTPUT"
DEPLOY_URL=$(echo "$OUTPUT" | grep -o 'https://[^ ]*netlify.app' | head -1)
echo "deploy-url=$DEPLOY_URL" >> $GITHUB_OUTPUT
- name: Deploy to Netlify
id: deploy
run: |
OUTPUT=$(netlify deploy \
--dir=./build \
--site=${{ secrets.NETLIFY_SITE_ID }} \
--auth=${{ secrets.NETLIFY_AUTH_TOKEN }} \
--prod \
--json)
DEPLOY_URL=$(echo "$OUTPUT" | jq -r '.deploy_url')
echo "deploy-url=$DEPLOY_URL" >> $GITHUB_OUTPUT
Deploys a Docusaurus sub-site from a monorepo as a branch deploy, isolated from the main site's config:
name: Deploy Documentation
on:
push:
branches: [doc]
concurrency:
group: doc-deploy
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install documentation dependencies
working-directory: doc
run: pnpm install --frozen-lockfile
- name: Build Docusaurus documentation
working-directory: doc
run: pnpm run build
- name: Install Netlify CLI
run: npm install -g netlify-cli@19
- name: Prepare deploy directory
run: |
mkdir -p deploy-output/doc
cp -r doc/build/* deploy-output/doc/
mkdir -p deploy-output/.empty-functions
cat > deploy-output/netlify.toml << 'TOML'
[build]
publish = "."
TOML
- name: Deploy to Netlify (branch deploy)
working-directory: deploy-output
env:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
run: |
netlify deploy \
--dir=. \
--functions=.empty-functions \
--site=$NETLIFY_SITE_ID \
--auth=$NETLIFY_AUTH_TOKEN \
--alias=doc \
--message="Documentation deploy: ${{ github.sha }}"
--filter=<package-name> flag to netlify deploynetlify linknetlify link does NOT support --filter flag and fails in monoreposnetlify link in monorepos. It's unnecessary when using --site flag with netlify deploy. The --site flag directly specifies the target site, making explicit linking redundant.--no-build does not exist in netlify-cli@19+netlify deploy does NOT build by default. Use --build only when you want to opt-in to building.Cause: The CLI picks up functions from the root netlify.toml config, even when deploying a sub-site that doesn't need functions
Solution: Use --functions=<empty-dir> to override. Create an empty directory and point to it:
mkdir -p .empty-functions
netlify deploy --functions=.empty-functions ...
netlify.toml has a redirect like /doc/* -> https://doc--mysite.netlify.app/doc/:splat with force = true. On the branch deploy itself, this redirect proxies to itself.netlify.toml that has no redirects. Use working-directory in the GitHub Actions step so the CLI finds the local config instead of the root one.--dir path or build artifacts not downloadedexport NETLIFY_AUTH_TOKEN=$(echo "$TOKEN" | tr -d '[:space:]')NETLIFY_SITE_ID - From Site settings -> General -> Site details -> API IDNETLIFY_AUTH_TOKEN - From User settings -> Applications -> Personal access tokensWhen using GitHub Actions for deployment, disable Netlify's built-in CI:
echo "Disabled"netlify.toml:[build]
command = "echo 'Build disabled - deploying from GitHub Actions'"
publish = "out"
For simpler cases without monorepo issues:
- name: Deploy to Netlify
uses: nwtgck/[email protected]
with:
publish-dir: ./out
production-deploy: true
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: 'Deploy - ${{ github.sha }}'
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
timeout-minutes: 5
Note: This action may not handle monorepo scenarios well - use CLI directly for those cases.
netlify devIn a monorepo, netlify dev requires --filter to select the project. Without it, the CLI enters an interactive project selection prompt that blocks non-interactive environments:
# This works - selects the correct package
netlify dev --functions=netlify/functions --offline --filter my-package
# This gets stuck at interactive prompt
netlify dev --functions=netlify/functions --offline
A typical package.json script:
{
"netlify:dev": "PNPM_DISABLE_TRUST_STORE=true pnpm --package=netlify-cli dlx netlify dev --functions=netlify/functions --offline --filter my-package"
}
netlify dev Does/.netlify/functions/* → functions servernetlify.toml redirect rules (e.g., /api/products → /.netlify/functions/get-products).env variables into the environmentdlxOn pnpm 10.x, pnpm dlx netlify-cli may fail with:
ERR_PNPM_TRUST_DOWNGRADE High-risk trust downgrade for "@netlify/[email protected]"
The PNPM_DISABLE_TRUST_STORE=true env var may not work with pnpm dlx even though pnpm config list shows trust-store=false. This is a pnpm 10.x behavior where dlx creates its own install context.
Workarounds:
Install netlify-cli globally instead of using pnpm dlx:
npm install -g netlify-cli
netlify dev --functions=netlify/functions --offline --filter my-package
Use npx (if the global install exists):
npx netlify-cli dev --functions=netlify/functions --offline --filter my-package
netlify dev Crash Workaroundnetlify dev may crash with "Netlify CLI has terminated unexpectedly" after the proxy starts (observed in v23.14.0+). The proxy on port 8888 starts successfully but terminates immediately.
Workaround: Run functions separately
Use netlify functions:serve to run just the functions server on port 9999, then run your framework dev server separately:
# Terminal 1: Start functions server (port 9999)
netlify functions:serve --functions=netlify/functions --offline --filter my-package
# Terminal 2: Start Next.js dev server (port 34434)
pnpm dev
Limitation: Without the netlify dev proxy, API rewrites from netlify.toml (e.g., /api/products → /.netlify/functions/get-products) won't work. The functions are accessible directly at:
http://localhost:9999/.netlify/functions/<function-name>
For frameworks with output: 'export' (static site generation), Next.js rewrites() cannot be used. In this case, test the functions directly at the port 9999 URL.
netlify functions:serve vs netlify dev| Feature | netlify dev | netlify functions:serve |
|---|---|---|
| Port | 8888 (proxy) | 9999 |
| Framework dev server | Auto-started | Must start separately |
| API rewrites (netlify.toml) | Applied | Not applied |
| .env injection | Yes | Yes |
| Stability | May crash (v23+) | Stable |
| --filter flag | Supported | Supported |
development
Link Claude Code skill names mentioned in a CodeGrid article (data/{series}/{n}.md) to the author's public claude-resources repo, pinned to the latest commit hash so links don't rot. Use when: (1) user says 'linkify cc resources', 'link the skills', 'link skill names', or invokes /dev-linkify-cc-resources; (2) editing a CodeGrid article that mentions `/commits`, `/pr-complete`, `/skill-creator` or other Claude Code skills and they should point to claude-resources. Only links skills that actually exist in the public repo; skips hypothetical examples and code blocks.
development
Second opinion from Claude Opus on a plan or approach. Use when: (1) Planning phase of /big-plan needs a higher-quality review than /codex-2nd / /gco-2nd / /gcoc-2nd, (2) User says 'opus 2nd' or 'opus opinion', (3) Wanting Anthropic's larger model to critique a plan. Spawns a general-purpose Agent with model: opus that reads the plan file and returns structured feedback. Anthropic quota — not free.
tools
AI-based testing via subagent + a per-task test-flow skill. Use when the user wants to verify something that mechanical assertions can't fully capture — image recognition, visual size/position comparison, animation smoothness, multi-step manual flows that need AI judgment. Triggers: 'AI-based test', 'AI test', 'visual verify', 'image recognition test', 'manual operation test', 'human-eye check', 'verify visually', 'compare screenshots', 'looks the same', 'looks correct'. The skill's job is to (1) author a focused test-flow skill that captures the exact procedure + verdict criteria, then (2) dispatch a verification subagent via the Agent tool that loads BOTH the test-flow skill AND a browser-driving skill (/verify-ui primary, /headless-browser fallback) so the subagent has clear context and consistent verdicts. NEVER uses `claude -p` — subagent dispatch goes through the Agent tool exclusively.
development
End-of-workflow audit of touched GitHub issues, PRs, and branches via a Sonnet subagent. Use when: (1) /big-plan, /x-as-pr, or /x-wt-teams finishes its main work and needs to verify every touched resource is in the right state (closed when done, kept when ongoing, deleted when dead), (2) User says 'cleanup resources', 'audit cleanup', or 'check what should be closed', (3) A long workflow ends and the manager wants a structured paper trail of what it closed/kept/deleted. Auto-execute by default — the Sonnet agent proposes, the manager (you) executes safe actions and prints a final report.