Skip to content
Development
Skill

/generate-runner

Generate a customized precommit runner for any ecosystem. Use when: non-Node projects need a runner, eject from generic runner, customize runner for project. Not for: running precommit (use /precommit), installing runner (handled by auto-install). Output: customized runner

From plugin
sd0x-dev-flow
18999 skills16 agents5 hooks
Install
$ npx -y skills add sd0xdev/sd0x-dev-flow --skill generate-runner --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/generate-runner

Context preview

The summary Claude sees to decide when to auto-load this skill.

Generate a customized precommit runner for any ecosystem. Use when: non-Node projects need a runner, eject from generic runner, customize runner for project. Not for: running precommit (use /precommit), installing runner (handled by auto-install). Output: customized runner

SKILL.md

generate-runner.SKILL.md
name: generate-runner
description: "Generate a customized precommit runner for any ecosystem. Use when: non-Node projects need a runner, eject from generic runner, customize runner for project. Not for: running precommit (use /precommit), installing runner (handled by auto-install). Output: customized runner script with eject header."
allowed-tools: Read, Grep, Glob, Bash(node:*), Bash(chmod:*), Bash(bash:*), Write, AskUserQuestion

Generate Runner

Trigger

  • Keywords: generate runner, create runner, custom runner, eject runner, runner for python, runner for rust, runner for go

When NOT to Use

| Need | Use Instead | |------|-------------| | Run precommit checks | `/precommit` or `/precommit-fast` | | Install existing runner | `/project-setup` (auto-installs) | | Configure the shipped runner's lint globs | Check first that the runtime runner has no `@generated_at` header, then edit `.claude/runner-config.json` — and set `lintArgMode` for the same script role, or the globs stay inert (see § Lint Argument Injection) |

Lint Argument Injection

**Scope: the runners this plugin ships** — `scripts/precommit-runner.js` and `scripts/verify-runner.js`. What `/precommit`, `/precommit-fast` and `/verify` actually execute is the runtime copy at `.claude/scripts/*`, installed by `/install-scripts`. A runner **generated** by this skill is written to the same path but is user-owned: its template does not append lint arguments, so `lintGlobs` / `lintArgMode` have **no effect** on it. The discriminator is the eject header (§ Step 4) — all three of `@generated_at`, `@plugin_version`, `@template`, each on its own line with a value, in the opening comment block. Read the file before configuring it.

**Known limitation.** A project that installed a runner **before** this fix keeps running the old version, which passes ESLint-only flags into whatever lint script the repo declares — a file-rewriting non-ESLint linter reads them as paths and edits sources. Fixing the plugin source does not reach that copy, and detecting it reliably needs per-release provenance, which is deliberately out of scope. Remedy: **`/install-scripts precommit-runner.js verify-runner.js --force`**, with `--dry-run` first to read the write set (the installer copies "scripts + dependencies" without defining the closure, so the dry run is the authoritative list). The runners are named rather than swept in by `--all`, which would force-overwrite every core script; confirm `lib/utils.js` — where the fixed runners decide whether to inject — appears in the report.

**The shipped runner appends nothing to your lint script unless you ask it to.** `lintGlobs` alone does nothing; it needs `lintArgMode` for the same script role.

| Setting | Where | Shape | |---------|-------|-------| | `lintArgMode` | `.claude/runner-config.json`, or `package.json` → `sd0x` | `{"lint": "eslint"}` / `{"lint:fix": "none"}` — keyed by **script role**, never a bare string. Accepted values: `"eslint"` and `"none"` | | `lintGlobs` | same two places | `["src/**/*.{ts,tsx,js,jsx}", …]` — applied only when that role's `lintArgMode` is `"eslint"` |

**Precedence, per role**: `.claude/runner-config.json` first, **first valid value wins**; `package.json` → `sd0x` is the fallback. An unusable value — a bare string, a value outside `eslint`/`none`, a non-role-keyed shape — is warned about and falls through rather than latching, so `"none"` in `.claude` can suppress an opt-in `package.json` declares. (An unreadable `.claude/runner-config.json` warns via `loadLintGlobs()`; an unreadable `package.json` stays silent.)

In `package.json`, to inject ESLint's flags and globs into `lint:fix` only:

{ "sd0x": { "lintArgMode": { "lint:fix": "eslint" }, "lintGlobs": ["src/**/*.{ts,js}"] } }

`precommit` reads the `lint:fix` role; `verify` reads `lint` — keyed separately because a repo routinely runs different engines for each (this one runs markdownlint for `lint:fix`).

**Why it is opt-in.** The runner used to inject by default and guess from the script text whether the recipient was ESLint. markdownlint-cli2 treats every unrecognised argument as a file glob, so under `--fix` it rewrote JavaScript as Markdown; one run corrupted 71 files in this repo. Four successive detection grammars each misclassified something, so detection was removed rather than deepened.

Workflow

flowchart LR
    A[Detect Ecosystem] --> B[Select Template]
    B --> C[Customize]
    C --> D[Write Runner]
    D --> E[Verify]

Step 1: Detect Ecosystem

Scan project root for manifest files:

| Manifest | Ecosystem | Template ID | |----------|-----------|-------------| | `pnpm-lock.yaml` | Node.js (pnpm) | `node-pnpm` | | `yarn.lock` | Node.js (yarn) | `node-yarn` | | `package-lock.json` or `package.json` | Node.js (npm) | `node-npm` | | `pyproject.toml` | Python | `python` | | `Cargo.toml` | Rust | `rust` | | `go.mod` | Go | `go` |

If multiple detected, prefer Node.js > Python > Rust > Go. If none detected, ask user.

Step 2: Select Template

Load template from `references/templates.md` for the detected ecosystem.

Step 3: Customize

Read project-specific configuration:

| Source | What | |--------|------| | `package.json` scripts | Lint command, test command, build command | | `.claude/runner-config.json` | **Not read for lint configuration.** Neither `lintGlobs` nor `lintArgMode` reaches a generated runner — the template does not append lint arguments at all (§ Lint Argument Injection), so listing either here would present an inert value as a customization input | | Lock file | Package manager selection |

Step 4: Write Runner

Write to `.claude/scripts/precommit-runner.js` (Node) or `.claude/scripts/precommit-runner.sh` (non-Node).

> **A non-Node runner is generated for manual invocation, not for `/precommit`.** `/precommit`, > `/precommit-fast` and `/verify` probe the **`.js`** path only and execute it on existence; none of > them detects a `.sh

Read more
Ships withsd0x-dev-flow

Language: English | 繁體中文 | 简体中文 | 日本語 | 한국어 | Español The harness layer for Claude Code. Let the model choose the path. Keep "done" verifiable. Full control plane on Claude Code. Skills-only distribution for Codex CLI and other compatible agents.

Get the whole plugin

Other skills on sd0x-dev-flow.