[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6KSE0W7P6PA892AY7YBC5N4":3,"skill-same:01M6KSE0W7P6PA892AY7YBC5N4":72},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":39,"linkOnly":40,"markdownContent":41,"markdownBody":42,"frontMatter":43,"refName":44,"commitSha":45,"skillDirectory":46,"forksCount":47,"repositoryHtmlUrl":48,"repositoryHomepageUrl":6,"ownerHtmlUrl":49,"ownerBlogUrl":50,"ownerDescription":6,"updatedAt":51,"install":52,"resources":65},"01M6KSE0W7P6PA892AY7YBC5N4","gtb-eslint-config",null,"ESLint configuration guidance for projects using @gtbuchanan\u002Feslint-config. Covers the configure() API and options, pre-commit isolated-environment setup via createRequire, the bundled plugin set, suppression conventions, the two-plugin Markdown lint split, gitignore-derived ignores, and the per-package vs. workspace-root config split. Trigger keywords - @gtbuchanan\u002Feslint-config, eslint.config.ts, configure, ESLintConfigureOptions, eslint-disable, eslint-disable-next-line, gitignore, defaultIgnores, eslint-config-flat-gitignore, markdownlint-disable, --max-warnings, dist\u002F.eslintcache, lint:eslint, ESLint flat config, ESLint suppression.","gtbuchanan\u002Ftooling","gtbuchanan","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F715687?v=4","01M6KSDRCB1XRM9B6QDZQRAFJ7","Taylor Buchanan","01M6KSDRHPP4GQYJDDTWE9V28T",0,"MIT","https:\u002F\u002Fgithub.com\u002Fgtbuchanan\u002Ftooling\u002Fblob\u002Fmain\u002Fpackages\u002Feslint-config\u002Fskills\u002Fgtb-eslint-config\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],"config","gtb eslint config","eslint","gitignore","commit","for","split","environment setup","via","trigger","plugin","next","lint","environment","projects","configure","api","pre-commit","SET","package","2026-11-10 10:12:30",false,"---\nname: gtb-eslint-config\ndescription: ESLint configuration guidance for projects using @gtbuchanan\u002Feslint-config. Covers the configure() API and options, pre-commit isolated-environment setup via createRequire, the bundled plugin set, suppression conventions, the two-plugin Markdown lint split, gitignore-derived ignores, and the per-package vs. workspace-root config split. Trigger keywords - @gtbuchanan\u002Feslint-config, eslint.config.ts, configure, ESLintConfigureOptions, eslint-disable, eslint-disable-next-line, gitignore, defaultIgnores, eslint-config-flat-gitignore, markdownlint-disable, --max-warnings, dist\u002F.eslintcache, lint:eslint, ESLint flat config, ESLint suppression.\n---\n\n# @gtbuchanan\u002Feslint-config\n\nShared ESLint flat-config factory for TypeScript projects. Bundles\n`typescript-eslint` strict + stylistic presets and a curated plugin set,\nplus Prettier-via-ESLint formatting for non-JS\u002FTS files.\n\n## Quickstart\n\nInstall peers and configure:\n\n```sh\npnpm add -D @gtbuchanan\u002Feslint-config eslint jiti\n```\n\n```typescript\n\u002F\u002F eslint.config.ts\nimport { configure } from '@gtbuchanan\u002Feslint-config';\n\nexport default configure({\n  tsconfigRootDir: import.meta.dirname,\n});\n```\n\n`configure()` returns `Promise\u003CLinter.Config[]>`; ESLint awaits async\nflat configs natively.\n\n## `configure()` options\n\nAll optional except `tsconfigRootDir` (recommended for type-aware rules):\n\n- **`tsconfigRootDir`** — Root directory for the TypeScript project\n  service. Pass `import.meta.dirname` from the config file.\n- **`target`** — `'server'` (default) or `'browser'`. Server enables\n  `require-unicode-regexp` with the `\u002Fv` flag. Browser enables\n  `no-console` and `no-alert`; entry points are exempt from `no-console`.\n- **`agentSkillsHost`** — Agent hosts whose `SKILL.md` frontmatter\n  extensions are accepted. A host name, a property map for a host the\n  plugin doesn't ship, or a list of either. Defaults to `'standard'`\n  (the bare Agent Skills spec). See below.\n- **`entryPoints`** — Glob patterns exempt from `process.exit` and\n  hashbang restrictions (and from `no-console` in browser mode).\n  Defaults to `**\u002Fbin\u002F**\u002F*.{js,mjs,cjs,ts,mts,cts}` and `**\u002Fscripts\u002F**\u002F*`.\n- **`gitignore`** — Derives ignore patterns from `.gitignore` via\n  `eslint-config-flat-gitignore`. Defaults to `true`. See below.\n- **`ignores`** — Global ignore patterns applied on top of the\n  `.gitignore`-derived ones, covering the **tracked** files another tool\n  owns the format of. Defaults to `defaultIgnores`: lockfiles\n  (`**\u002F*-lock.json`, `**\u002F*-lock.yaml`, `**\u002F*.lock`, `**\u002F*.lock.json`,\n  `**\u002Fnpm-shrinkwrap.json`) and `**\u002FCHANGELOG.md`. Changesets formats the\n  changelogs it generates with its own Prettier resolution, so linting\n  them here reports diffs that are unfixable at the source. Passing this\n  option replaces the list wholesale — spread `defaultIgnores` to extend\n  it.\n- **`onlyWarn`** — Downgrades all errors to warnings via\n  `eslint-plugin-only-warn`. Defaults to `true`. Irreversible within\n  a process — uses a side-effect import that monkey-patches the ESLint\n  Linter class.\n- **`pnpm`** — Enables `eslint-plugin-pnpm` rules for `package.json`\n  and `pnpm-workspace.yaml`. Defaults to `true`.\n- **`pnpmWorkspaceSettings`** — Policy for `pnpm\u002Fyaml-enforce-settings`,\n  which lints the settings keys of `pnpm-workspace.yaml`. Defaults to\n  `defaultPnpmWorkspaceSettings` (see below). Pass `false` to keep the\n  catalog rules but drop the settings policy. Ignored when `pnpm` is\n  `false`.\n\n## Gitignore-derived ignores\n\n`configure()` reads `.gitignore` through\n[`eslint-config-flat-gitignore`](https:\u002F\u002Fgithub.com\u002Fantfu\u002Feslint-config-flat-gitignore)\nand contributes the converted patterns as a global-ignores entry named\n`gitignore`. This is why `ignores` lists only tracked files: build output,\ncaches, and generated files are already untracked, so `.gitignore` is the\nsingle source of truth for them.\n\nTwo defaults differ from upstream, and a caller-supplied options object is\nmerged over them (unlike `ignores`, partial overrides keep the rest):\n\n- **`recursive: { skipDirs: ['.turbo', 'coverage', 'dist'] }`** — also\n  picks up per-package `.gitignore` files, so a workspace-root lint run\n  honors the ones nested in each package. Discovery is a directory walk\n  that doesn't apply the rules it loads, so it skips build output by name\n  (at any depth): a concurrent build deleting a directory mid-walk would\n  otherwise fail config loading with `ENOENT`. Overriding `skipDirs`\n  replaces the list, so repeat these names when adding your own.\n- **`strict: false`** — upstream throws when an ignore file is missing.\n  A shared config is loaded by repos it knows nothing about, so a missing\n  `.gitignore` contributes no patterns rather than failing the lint run.\n\n```typescript\nexport default configure({ gitignore: false }); \u002F\u002F opt out entirely\nexport default configure({\n  gitignore: { files: ['.gitignore', '.eslintignore'] },\n});\n```\n\nOpting out leaves only `ignores`, which no longer covers build output —\na repo that does so has to enumerate its own generated paths.\n\n### Why per-package lint sees fewer patterns\n\nPatterns are resolved relative to the directory ESLint runs in, and a\n`.gitignore` entry containing a mid-string `\u002F` is anchored to the\ndirectory of the file that declared it. So when a per-package config\nwalks up to the workspace-root `.gitignore`:\n\n- Depth-agnostic entries survive — `dist\u002F` becomes `**\u002Fdist\u002F`, which\n  still matches inside the package.\n- Root-anchored entries are **dropped** — `.claude\u002Fworktrees\u002F` names a\n  path outside the package, and no ESLint ignore pattern can reach a\n  parent directory.\n\nNothing is lost, because the root config lints those root paths. It does\nmean the `gitignore` entry legitimately holds different patterns\ndepending on which config loaded it.\n\n## Agent Skills frontmatter hosts\n\n`SKILL.md` frontmatter is validated against the\n[Agent Skills spec](https:\u002F\u002Fagentskills.io\u002Fspecification), which closes\nthe object to unknown properties. The spec sanctions only the nested\n`metadata` map for client-specific data, so a host that puts fields at\nthe top level — as Claude Code does — needs each one declared. There is\nno reserved top-level namespace to pattern-match instead, which is why\nthis is a per-host list rather than a wildcard.\n\n```typescript\nexport default configure({ agentSkillsHost: 'claude-code' });\n```\n\nSkills that target several hosts list them all — every name the plugin\nships, plus a JSON Schema property map for any host it doesn't. Fields\nare unioned, so the result accepts every field any listed source\ndeclares:\n\n```typescript\nexport default configure({\n  agentSkillsHost: ['claude-code', { 'x-team-owner': { type: 'string' } }],\n});\n```\n\nAn unrecognized host name throws rather than silently contributing no\nfields, so a typo surfaces at config load rather than as unexplained\nfrontmatter errors.\n\n`'claude-code'` adds the fields from Claude Code's\n[frontmatter reference](https:\u002F\u002Fcode.claude.com\u002Fdocs\u002Fen\u002Fskills), plus\nthe YAML-list spelling it accepts for `allowed-tools`. The plugin's\n`skillFrontmatterHosts` registry is what each name resolves to.\n\nEvery form adds properties only: `name` and `description` stay required\nand every shared constraint still applies, so a skill that validates\nunder a host is still portable to a spec-only host apart from the\nextension fields themselves. There is no \"valid under every host at\nonce\" setting — that is `'standard'`, the default, which is also what\nkeeps a stray `user_invocable` typo an error rather than a\nsilently-ignored field.\n\n## pnpm workspace settings policy\n\n`pnpm\u002Fyaml-enforce-settings` lints the settings keys of\n`pnpm-workspace.yaml` — the block that absorbed most of `.npmrc` in\npnpm 10. The other `eslint-plugin-pnpm` rules cover only catalogs and\npackage globs, so this is the one that keeps install behavior\nconsistent across repos. `defaultPnpmWorkspaceSettings` enforces:\n\n- `settings` (exact key **and** value, auto-fixable) —\n  `engineStrict: true`, `hoist: false`, `minimumReleaseAge: 4320`\n  (3 days, matching the shared Renovate preset), and\n  `strictPeerDependencies: true`\n- `requiredFields` (key must exist, any value, never fixed) —\n  `minimumReleaseAgeExclude`\n- `forbiddenFields` (key must not exist, never fixed) —\n  `dangerouslyAllowAllBuilds`, `publicHoistPattern`, `shamefullyHoist`,\n  `trustLockfile`\n\nEach `settings` entry is inserted into the consumer's file by `--fix`,\nso the set is kept to values that differ from pnpm's own defaults.\nSettings that merely restate a default add a line that changes nothing.\n\n### Why the unsafe settings are forbidden keys\n\n`settings` can only express exact equality — there is no \"anything but\nthis value\". A setting that is dangerous in one direction therefore has\nto be banned outright by key. `dangerouslyAllowAllBuilds` runs every\ndependency's install scripts without approval, bypassing `allowBuilds`.\n`publicHoistPattern` and `shamefullyHoist` both flatten dependencies\ninto the root `node_modules`, undoing `hoist: false` and letting\nundeclared imports resolve — pnpm defines `shamefullyHoist` as\n`publicHoistPattern: '*'`, so banning one without the other leaves the\nhole open. `trustLockfile` skips the lockfile's supply-chain\nverification pass.\n\nBecause these are key-presence checks, they also flag a harmless\nexplicit value (`publicHoistPattern: []`), and a key absent from an\nolder pnpm simply never appears.\n\n### Why the exclude list is required rather than value-matched\n\n`settings` compares whole values with deep equality, and its fixer\nreplaces the entire key\u002Fvalue pair. For a list a consumer legitimately\nextends, that combination is destructive: adding `'@acme\u002F*'` to\n`minimumReleaseAgeExclude` would read as a mismatch, and `--fix` would\ndelete it. Requiring the key without pinning its value keeps the\nsetting deliberate while leaving its contents to the consumer. Apply\nthe same reasoning before moving any future list-valued setting into\n`settings`.\n\n### Overriding the policy\n\n`pnpmWorkspaceSettings` **replaces** the default rather than merging\ninto it, and spread is shallow — a bare `...defaultPnpmWorkspaceSettings`\nstill clobbers `settings` wholesale. Spread both levels:\n\n```typescript\nimport {\n  configure,\n  defaultPnpmWorkspaceSettings as base,\n} from '@gtbuchanan\u002Feslint-config';\n\nexport default configure({\n  pnpmWorkspaceSettings: {\n    ...base,\n    settings: { ...base.settings, hoist: true },\n  },\n});\n```\n\nA policy with no `settings`, `requiredFields`, or `forbiddenFields`\ndrops the rule instead of enabling it — the rule throws when given an\nempty policy, so `{}` and `false` behave the same.\n\n## Pre-commit isolation\n\n[pre-commit](https:\u002F\u002Fpre-commit.com\u002F) and [prek](https:\u002F\u002Fprek.dev) run\nhooks in an isolated environment where the project's `node_modules` is\nnot available — the import `'@gtbuchanan\u002Feslint-config'` would fail\nunder default ESM resolution. ([hk](https:\u002F\u002Fhk.jdx.dev) runs steps in\nthe repo with `node_modules` present, so a `pnpm exec eslint` step\nneeds none of the below.)\n\nUnder pre-commit\u002Fprek, use `createRequire` to bridge ESM → CJS\nresolution, which respects the `NODE_PATH` the hook manager sets:\n\n```typescript\n\u002F\u002F eslint.config.ts\nimport { createRequire } from 'node:module';\nimport { pathToFileURL } from 'node:url';\nimport type * as EslintConfig from '@gtbuchanan\u002Feslint-config';\n\ninterface ModuleMap {\n  '@gtbuchanan\u002Feslint-config': typeof EslintConfig;\n}\n\nconst { resolve } = createRequire(import.meta.url);\n\nasync function importModule\u003CS extends keyof ModuleMap>(\n  specifier: S,\n): Promise\u003CModuleMap[S]> {\n  const { href } = pathToFileURL(resolve(specifier));\n  const module: ModuleMap[S] = await import(href);\n  return module;\n}\n\nconst { configure } = await importModule('@gtbuchanan\u002Feslint-config');\n\nexport default configure({\n  tsconfigRootDir: import.meta.dirname,\n});\n```\n\n## Bundled plugins\n\nEnabled by `configure()`. Each is a separate concern; per-plugin rule\ndepth lives in that plugin's own skill (where one exists).\n\n- **`typescript-eslint`** — `strictTypeChecked` + `stylisticTypeChecked`\n- **`eslint-plugin-unicorn`** — recommended modern JS\u002FTS rules\n- **`eslint-plugin-promise`** — promise hygiene\n- **`eslint-plugin-regexp`** — regex correctness and safety\n- **`eslint-plugin-jsdoc`** — JSDoc\u002FTSDoc validation\n- **`@stylistic\u002Feslint-plugin`** — JS\u002FTS formatting (semicolons, quotes,\n  spacing)\n- **`eslint-plugin-format`** — Prettier formatting for JSON, Markdown,\n  YAML, CSS, XML via ESLint rules. JS\u002FTS formatting goes through\n  `@stylistic` instead.\n- **`@eslint-community\u002Feslint-plugin-eslint-comments`** — suppression\n  comment hygiene\n- **`eslint-plugin-import-x`** — `recommended` + the plugin's\n  `typescript` preset, plus ordering and hygiene rules\n  (`no-self-import`, `no-mutable-exports`, …). The resolution-based\n  rules from the preset (`no-unresolved`, `default`, `export`,\n  `namespace`) are switched back off: TypeScript already reports them\n  and they force full module resolution per lint.\n- **`@eslint\u002Fjson`** — JSON file linting\n- **`eslint-plugin-pnpm`** — workspace validation (gated by the `pnpm`\n  option)\n- **`eslint-plugin-n`** — Node.js best practices\n- **`eslint-plugin-yml`** — YAML linting + key sorting\n- **`@gtbuchanan\u002Feslint-plugin-yamllint`** — yamllint gap rules\n  (truthy, octal-values, anchors, document-start\u002Fend)\n- **`@eslint\u002Fmarkdown`** — official Markdown plugin (commonmark AST,\n  recommended rule set)\n- **`@gtbuchanan\u002Feslint-plugin-markdownlint`** — Markdown structural\n  linting for the rules `@eslint\u002Fmarkdown` doesn't cover yet\n- **`@gtbuchanan\u002Feslint-plugin-md-frontmatter`** — Markdown frontmatter\n  validation via JSON Schema (ajv-backed)\n- **`@gtbuchanan\u002Feslint-plugin-agent-skills`** — Agent Skills\n  frontmatter schema + spec rules; plugs into `md-frontmatter`\n- **`@vitest\u002Feslint-plugin`** — test-specific rules\n- **`eslint-config-flat-gitignore`** — turns `.gitignore` into global\n  ignores (gated by the `gitignore` option). A config, not a plugin — it\n  contributes no rules.\n- **`eslint-plugin-only-warn`** — downgrades errors to warnings (gated\n  by the `onlyWarn` option)\n\nPrettier plugins (`prettier-plugin-sort-json`,\n`prettier-plugin-multiline-arrays`, `prettier-plugin-packagejson`,\n`prettier-plugin-css-order`, `@prettier\u002Fplugin-xml`) are resolved as\n`file:\u002F\u002F` URLs from this package's dependencies for reliable resolution\nunder pnpm strict hoisting.\n\n## Conventions\n\n- **Warnings-only in IDE, errors in CI.** `onlyWarn: true` (the default)\n  surfaces every lint violation as a warning so TypeScript diagnostics\n  stand out in editors. CI runs `eslint --max-warnings=0` to enforce\n  zero violations. The `gtb task lint:eslint` command sets this flag\n  along with `--cache --cache-location dist\u002F.eslintcache`.\n- **Inline suppressions require a `--` reason suffix.** Enforced by\n  `@eslint-community\u002Feslint-plugin-eslint-comments`. Use the multiline\n  format for readability:\n\n  ```ts\n  \u002F* eslint-disable-next-line rule-name-1, rule-name-2 --\n     This is my reason *\u002F\n  ```\n\n- **Prefer `eslint-disable-next-line` over `eslint-disable`.** Scope\n  suppressions to the narrowest possible range.\n- **All exported functions, types, interfaces, and constants must have\n  JSDoc comments.** Enforced by `eslint-plugin-jsdoc`.\n\n## Markdown lint split\n\nTwo plugins lint Markdown together:\n\n- `@eslint\u002Fmarkdown` runs its `recommended` rule set (CommonMark AST).\n- `@gtbuchanan\u002Feslint-plugin-markdownlint` fills the structural gaps\n  `@eslint\u002Fmarkdown` doesn't cover yet. Rules `@eslint\u002Fmarkdown`\n  already enforces are disabled in the markdownlint plugin to keep\n  diagnostics single-sourced. As `@eslint\u002Fmarkdown` adds rules upstream,\n  the markdownlint counterparts retire one by one.\n\nSuppressions use different syntax for each:\n\n```markdown\n\u003C!-- eslint-disable-next-line markdown\u002Fno-duplicate-headings --\n     intentional -->\n\n# Duplicate heading allowed here\n```\n\n```markdown\n\u003C!-- markdownlint-disable-next-line MD036 -->\n\n**Bold paragraph used as heading**\n```\n\n`markdownlint\u002Flint` runs as a single ESLint rule, so per-rule control\nmust use markdownlint's own directive — `\u003C!-- eslint-disable\nmarkdownlint\u002Flint -->` would suppress every markdownlint rule at once.\n\n## Per-package vs. workspace-root config\n\nMonorepos using `@gtbuchanan\u002Fcli` follow a two-tier ESLint setup:\n\n- **Per-package `eslint.config.ts`** — calls `configure()` and lints\n  source under that package. The generated `lint:eslint` task runs with\n  `--cache --cache-location dist\u002F.eslintcache --max-warnings=0`. Cache\n  files live under each package's `dist\u002F`.\n- **Root `eslint.config.ts`** — when present, `gtb sync` also generates\n  a `\u002F\u002F#lint:eslint` turbo task and a root `lint:eslint` script. The\n  root script lints workspace-root files (`package.json`,\n  `pnpm-workspace.yaml`, `.github\u002F`, etc.) that per-package lint never\n  sees. Per-package directories are excluded automatically via\n  `--ignore-pattern` flags derived from `pnpm-workspace.yaml` package\n  globs — no manual upkeep.\n\nSingle-package repos only need the root config.\n","# @gtbuchanan\u002Feslint-config\n\nShared ESLint flat-config factory for TypeScript projects. Bundles\n`typescript-eslint` strict + stylistic presets and a curated plugin set,\nplus Prettier-via-ESLint formatting for non-JS\u002FTS files.\n\n## Quickstart\n\nInstall peers and configure:\n\n```sh\npnpm add -D @gtbuchanan\u002Feslint-config eslint jiti\n```\n\n```typescript\n\u002F\u002F eslint.config.ts\nimport { configure } from '@gtbuchanan\u002Feslint-config';\n\nexport default configure({\n  tsconfigRootDir: import.meta.dirname,\n});\n```\n\n`configure()` returns `Promise\u003CLinter.Config[]>`; ESLint awaits async\nflat configs natively.\n\n## `configure()` options\n\nAll optional except `tsconfigRootDir` (recommended for type-aware rules):\n\n- **`tsconfigRootDir`** — Root directory for the TypeScript project\n  service. Pass `import.meta.dirname` from the config file.\n- **`target`** — `'server'` (default) or `'browser'`. Server enables\n  `require-unicode-regexp` with the `\u002Fv` flag. Browser enables\n  `no-console` and `no-alert`; entry points are exempt from `no-console`.\n- **`agentSkillsHost`** — Agent hosts whose `SKILL.md` frontmatter\n  extensions are accepted. A host name, a property map for a host the\n  plugin doesn't ship, or a list of either. Defaults to `'standard'`\n  (the bare Agent Skills spec). See below.\n- **`entryPoints`** — Glob patterns exempt from `process.exit` and\n  hashbang restrictions (and from `no-console` in browser mode).\n  Defaults to `**\u002Fbin\u002F**\u002F*.{js,mjs,cjs,ts,mts,cts}` and `**\u002Fscripts\u002F**\u002F*`.\n- **`gitignore`** — Derives ignore patterns from `.gitignore` via\n  `eslint-config-flat-gitignore`. Defaults to `true`. See below.\n- **`ignores`** — Global ignore patterns applied on top of the\n  `.gitignore`-derived ones, covering the **tracked** files another tool\n  owns the format of. Defaults to `defaultIgnores`: lockfiles\n  (`**\u002F*-lock.json`, `**\u002F*-lock.yaml`, `**\u002F*.lock`, `**\u002F*.lock.json`,\n  `**\u002Fnpm-shrinkwrap.json`) and `**\u002FCHANGELOG.md`. Changesets formats the\n  changelogs it generates with its own Prettier resolution, so linting\n  them here reports diffs that are unfixable at the source. Passing this\n  option replaces the list wholesale — spread `defaultIgnores` to extend\n  it.\n- **`onlyWarn`** — Downgrades all errors to warnings via\n  `eslint-plugin-only-warn`. Defaults to `true`. Irreversible within\n  a process — uses a side-effect import that monkey-patches the ESLint\n  Linter class.\n- **`pnpm`** — Enables `eslint-plugin-pnpm` rules for `package.json`\n  and `pnpm-workspace.yaml`. Defaults to `true`.\n- **`pnpmWorkspaceSettings`** — Policy for `pnpm\u002Fyaml-enforce-settings`,\n  which lints the settings keys of `pnpm-workspace.yaml`. Defaults to\n  `defaultPnpmWorkspaceSettings` (see below). Pass `false` to keep the\n  catalog rules but drop the settings policy. Ignored when `pnpm` is\n  `false`.\n\n## Gitignore-derived ignores\n\n`configure()` reads `.gitignore` through\n[`eslint-config-flat-gitignore`](https:\u002F\u002Fgithub.com\u002Fantfu\u002Feslint-config-flat-gitignore)\nand contributes the converted patterns as a global-ignores entry named\n`gitignore`. This is why `ignores` lists only tracked files: build output,\ncaches, and generated files are already untracked, so `.gitignore` is the\nsingle source of truth for them.\n\nTwo defaults differ from upstream, and a caller-supplied options object is\nmerged over them (unlike `ignores`, partial overrides keep the rest):\n\n- **`recursive: { skipDirs: ['.turbo', 'coverage', 'dist'] }`** — also\n  picks up per-package `.gitignore` files, so a workspace-root lint run\n  honors the ones nested in each package. Discovery is a directory walk\n  that doesn't apply the rules it loads, so it skips build output by name\n  (at any depth): a concurrent build deleting a directory mid-walk would\n  otherwise fail config loading with `ENOENT`. Overriding `skipDirs`\n  replaces the list, so repeat these names when adding your own.\n- **`strict: false`** — upstream throws when an ignore file is missing.\n  A shared config is loaded by repos it knows nothing about, so a missing\n  `.gitignore` contributes no patterns rather than failing the lint run.\n\n```typescript\nexport default configure({ gitignore: false }); \u002F\u002F opt out entirely\nexport default configure({\n  gitignore: { files: ['.gitignore', '.eslintignore'] },\n});\n```\n\nOpting out leaves only `ignores`, which no longer covers build output —\na repo that does so has to enumerate its own generated paths.\n\n### Why per-package lint sees fewer patterns\n\nPatterns are resolved relative to the directory ESLint runs in, and a\n`.gitignore` entry containing a mid-string `\u002F` is anchored to the\ndirectory of the file that declared it. So when a per-package config\nwalks up to the workspace-root `.gitignore`:\n\n- Depth-agnostic entries survive — `dist\u002F` becomes `**\u002Fdist\u002F`, which\n  still matches inside the package.\n- Root-anchored entries are **dropped** — `.claude\u002Fworktrees\u002F` names a\n  path outside the package, and no ESLint ignore pattern can reach a\n  parent directory.\n\nNothing is lost, because the root config lints those root paths. It does\nmean the `gitignore` entry legitimately holds different patterns\ndepending on which config loaded it.\n\n## Agent Skills frontmatter hosts\n\n`SKILL.md` frontmatter is validated against the\n[Agent Skills spec](https:\u002F\u002Fagentskills.io\u002Fspecification), which closes\nthe object to unknown properties. The spec sanctions only the nested\n`metadata` map for client-specific data, so a host that puts fields at\nthe top level — as Claude Code does — needs each one declared. There is\nno reserved top-level namespace to pattern-match instead, which is why\nthis is a per-host list rather than a wildcard.\n\n```typescript\nexport default configure({ agentSkillsHost: 'claude-code' });\n```\n\nSkills that target several hosts list them all — every name the plugin\nships, plus a JSON Schema property map for any host it doesn't. Fields\nare unioned, so the result accepts every field any listed source\ndeclares:\n\n```typescript\nexport default configure({\n  agentSkillsHost: ['claude-code', { 'x-team-owner': { type: 'string' } }],\n});\n```\n\nAn unrecognized host name throws rather than silently contributing no\nfields, so a typo surfaces at config load rather than as unexplained\nfrontmatter errors.\n\n`'claude-code'` adds the fields from Claude Code's\n[frontmatter reference](https:\u002F\u002Fcode.claude.com\u002Fdocs\u002Fen\u002Fskills), plus\nthe YAML-list spelling it accepts for `allowed-tools`. The plugin's\n`skillFrontmatterHosts` registry is what each name resolves to.\n\nEvery form adds properties only: `name` and `description` stay required\nand every shared constraint still applies, so a skill that validates\nunder a host is still portable to a spec-only host apart from the\nextension fields themselves. There is no \"valid under every host at\nonce\" setting — that is `'standard'`, the default, which is also what\nkeeps a stray `user_invocable` typo an error rather than a\nsilently-ignored field.\n\n## pnpm workspace settings policy\n\n`pnpm\u002Fyaml-enforce-settings` lints the settings keys of\n`pnpm-workspace.yaml` — the block that absorbed most of `.npmrc` in\npnpm 10. The other `eslint-plugin-pnpm` rules cover only catalogs and\npackage globs, so this is the one that keeps install behavior\nconsistent across repos. `defaultPnpmWorkspaceSettings` enforces:\n\n- `settings` (exact key **and** value, auto-fixable) —\n  `engineStrict: true`, `hoist: false`, `minimumReleaseAge: 4320`\n  (3 days, matching the shared Renovate preset), and\n  `strictPeerDependencies: true`\n- `requiredFields` (key must exist, any value, never fixed) —\n  `minimumReleaseAgeExclude`\n- `forbiddenFields` (key must not exist, never fixed) —\n  `dangerouslyAllowAllBuilds`, `publicHoistPattern`, `shamefullyHoist`,\n  `trustLockfile`\n\nEach `settings` entry is inserted into the consumer's file by `--fix`,\nso the set is kept to values that differ from pnpm's own defaults.\nSettings that merely restate a default add a line that changes nothing.\n\n### Why the unsafe settings are forbidden keys\n\n`settings` can only express exact equality — there is no \"anything but\nthis value\". A setting that is dangerous in one direction therefore has\nto be banned outright by key. `dangerouslyAllowAllBuilds` runs every\ndependency's install scripts without approval, bypassing `allowBuilds`.\n`publicHoistPattern` and `shamefullyHoist` both flatten dependencies\ninto the root `node_modules`, undoing `hoist: false` and letting\nundeclared imports resolve — pnpm defines `shamefullyHoist` as\n`publicHoistPattern: '*'`, so banning one without the other leaves the\nhole open. `trustLockfile` skips the lockfile's supply-chain\nverification pass.\n\nBecause these are key-presence checks, they also flag a harmless\nexplicit value (`publicHoistPattern: []`), and a key absent from an\nolder pnpm simply never appears.\n\n### Why the exclude list is required rather than value-matched\n\n`settings` compares whole values with deep equality, and its fixer\nreplaces the entire key\u002Fvalue pair. For a list a consumer legitimately\nextends, that combination is destructive: adding `'@acme\u002F*'` to\n`minimumReleaseAgeExclude` would read as a mismatch, and `--fix` would\ndelete it. Requiring the key without pinning its value keeps the\nsetting deliberate while leaving its contents to the consumer. Apply\nthe same reasoning before moving any future list-valued setting into\n`settings`.\n\n### Overriding the policy\n\n`pnpmWorkspaceSettings` **replaces** the default rather than merging\ninto it, and spread is shallow — a bare `...defaultPnpmWorkspaceSettings`\nstill clobbers `settings` wholesale. Spread both levels:\n\n```typescript\nimport {\n  configure,\n  defaultPnpmWorkspaceSettings as base,\n} from '@gtbuchanan\u002Feslint-config';\n\nexport default configure({\n  pnpmWorkspaceSettings: {\n    ...base,\n    settings: { ...base.settings, hoist: true },\n  },\n});\n```\n\nA policy with no `settings`, `requiredFields`, or `forbiddenFields`\ndrops the rule instead of enabling it — the rule throws when given an\nempty policy, so `{}` and `false` behave the same.\n\n## Pre-commit isolation\n\n[pre-commit](https:\u002F\u002Fpre-commit.com\u002F) and [prek](https:\u002F\u002Fprek.dev) run\nhooks in an isolated environment where the project's `node_modules` is\nnot available — the import `'@gtbuchanan\u002Feslint-config'` would fail\nunder default ESM resolution. ([hk](https:\u002F\u002Fhk.jdx.dev) runs steps in\nthe repo with `node_modules` present, so a `pnpm exec eslint` step\nneeds none of the below.)\n\nUnder pre-commit\u002Fprek, use `createRequire` to bridge ESM → CJS\nresolution, which respects the `NODE_PATH` the hook manager sets:\n\n```typescript\n\u002F\u002F eslint.config.ts\nimport { createRequire } from 'node:module';\nimport { pathToFileURL } from 'node:url';\nimport type * as EslintConfig from '@gtbuchanan\u002Feslint-config';\n\ninterface ModuleMap {\n  '@gtbuchanan\u002Feslint-config': typeof EslintConfig;\n}\n\nconst { resolve } = createRequire(import.meta.url);\n\nasync function importModule\u003CS extends keyof ModuleMap>(\n  specifier: S,\n): Promise\u003CModuleMap[S]> {\n  const { href } = pathToFileURL(resolve(specifier));\n  const module: ModuleMap[S] = await import(href);\n  return module;\n}\n\nconst { configure } = await importModule('@gtbuchanan\u002Feslint-config');\n\nexport default configure({\n  tsconfigRootDir: import.meta.dirname,\n});\n```\n\n## Bundled plugins\n\nEnabled by `configure()`. Each is a separate concern; per-plugin rule\ndepth lives in that plugin's own skill (where one exists).\n\n- **`typescript-eslint`** — `strictTypeChecked` + `stylisticTypeChecked`\n- **`eslint-plugin-unicorn`** — recommended modern JS\u002FTS rules\n- **`eslint-plugin-promise`** — promise hygiene\n- **`eslint-plugin-regexp`** — regex correctness and safety\n- **`eslint-plugin-jsdoc`** — JSDoc\u002FTSDoc validation\n- **`@stylistic\u002Feslint-plugin`** — JS\u002FTS formatting (semicolons, quotes,\n  spacing)\n- **`eslint-plugin-format`** — Prettier formatting for JSON, Markdown,\n  YAML, CSS, XML via ESLint rules. JS\u002FTS formatting goes through\n  `@stylistic` instead.\n- **`@eslint-community\u002Feslint-plugin-eslint-comments`** — suppression\n  comment hygiene\n- **`eslint-plugin-import-x`** — `recommended` + the plugin's\n  `typescript` preset, plus ordering and hygiene rules\n  (`no-self-import`, `no-mutable-exports`, …). The resolution-based\n  rules from the preset (`no-unresolved`, `default`, `export`,\n  `namespace`) are switched back off: TypeScript already reports them\n  and they force full module resolution per lint.\n- **`@eslint\u002Fjson`** — JSON file linting\n- **`eslint-plugin-pnpm`** — workspace validation (gated by the `pnpm`\n  option)\n- **`eslint-plugin-n`** — Node.js best practices\n- **`eslint-plugin-yml`** — YAML linting + key sorting\n- **`@gtbuchanan\u002Feslint-plugin-yamllint`** — yamllint gap rules\n  (truthy, octal-values, anchors, document-start\u002Fend)\n- **`@eslint\u002Fmarkdown`** — official Markdown plugin (commonmark AST,\n  recommended rule set)\n- **`@gtbuchanan\u002Feslint-plugin-markdownlint`** — Markdown structural\n  linting for the rules `@eslint\u002Fmarkdown` doesn't cover yet\n- **`@gtbuchanan\u002Feslint-plugin-md-frontmatter`** — Markdown frontmatter\n  validation via JSON Schema (ajv-backed)\n- **`@gtbuchanan\u002Feslint-plugin-agent-skills`** — Agent Skills\n  frontmatter schema + spec rules; plugs into `md-frontmatter`\n- **`@vitest\u002Feslint-plugin`** — test-specific rules\n- **`eslint-config-flat-gitignore`** — turns `.gitignore` into global\n  ignores (gated by the `gitignore` option). A config, not a plugin — it\n  contributes no rules.\n- **`eslint-plugin-only-warn`** — downgrades errors to warnings (gated\n  by the `onlyWarn` option)\n\nPrettier plugins (`prettier-plugin-sort-json`,\n`prettier-plugin-multiline-arrays`, `prettier-plugin-packagejson`,\n`prettier-plugin-css-order`, `@prettier\u002Fplugin-xml`) are resolved as\n`file:\u002F\u002F` URLs from this package's dependencies for reliable resolution\nunder pnpm strict hoisting.\n\n## Conventions\n\n- **Warnings-only in IDE, errors in CI.** `onlyWarn: true` (the default)\n  surfaces every lint violation as a warning so TypeScript diagnostics\n  stand out in editors. CI runs `eslint --max-warnings=0` to enforce\n  zero violations. The `gtb task lint:eslint` command sets this flag\n  along with `--cache --cache-location dist\u002F.eslintcache`.\n- **Inline suppressions require a `--` reason suffix.** Enforced by\n  `@eslint-community\u002Feslint-plugin-eslint-comments`. Use the multiline\n  format for readability:\n\n  ```ts\n  \u002F* eslint-disable-next-line rule-name-1, rule-name-2 --\n     This is my reason *\u002F\n  ```\n\n- **Prefer `eslint-disable-next-line` over `eslint-disable`.** Scope\n  suppressions to the narrowest possible range.\n- **All exported functions, types, interfaces, and constants must have\n  JSDoc comments.** Enforced by `eslint-plugin-jsdoc`.\n\n## Markdown lint split\n\nTwo plugins lint Markdown together:\n\n- `@eslint\u002Fmarkdown` runs its `recommended` rule set (CommonMark AST).\n- `@gtbuchanan\u002Feslint-plugin-markdownlint` fills the structural gaps\n  `@eslint\u002Fmarkdown` doesn't cover yet. Rules `@eslint\u002Fmarkdown`\n  already enforces are disabled in the markdownlint plugin to keep\n  diagnostics single-sourced. As `@eslint\u002Fmarkdown` adds rules upstream,\n  the markdownlint counterparts retire one by one.\n\nSuppressions use different syntax for each:\n\n```markdown\n\u003C!-- eslint-disable-next-line markdown\u002Fno-duplicate-headings --\n     intentional -->\n\n# Duplicate heading allowed here\n```\n\n```markdown\n\u003C!-- markdownlint-disable-next-line MD036 -->\n\n**Bold paragraph used as heading**\n```\n\n`markdownlint\u002Flint` runs as a single ESLint rule, so per-rule control\nmust use markdownlint's own directive — `\u003C!-- eslint-disable\nmarkdownlint\u002Flint -->` would suppress every markdownlint rule at once.\n\n## Per-package vs. workspace-root config\n\nMonorepos using `@gtbuchanan\u002Fcli` follow a two-tier ESLint setup:\n\n- **Per-package `eslint.config.ts`** — calls `configure()` and lints\n  source under that package. The generated `lint:eslint` task runs with\n  `--cache --cache-location dist\u002F.eslintcache --max-warnings=0`. Cache\n  files live under each package's `dist\u002F`.\n- **Root `eslint.config.ts`** — when present, `gtb sync` also generates\n  a `\u002F\u002F#lint:eslint` turbo task and a root `lint:eslint` script. The\n  root script lints workspace-root files (`package.json`,\n  `pnpm-workspace.yaml`, `.github\u002F`, etc.) that per-package lint never\n  sees. Per-package directories are excluded automatically via\n  `--ignore-pattern` flags derived from `pnpm-workspace.yaml` package\n  globs — no manual upkeep.\n\nSingle-package repos only need the root config.\n",{"name":5,"description":7},"main","06b998b2d006e75e47014ea048748d5660f6e7bf","packages\u002Feslint-config\u002Fskills\u002Fgtb-eslint-config",1,"https:\u002F\u002Fgithub.com\u002Fgtbuchanan\u002Ftooling","https:\u002F\u002Fgithub.com\u002Fgtbuchanan","https:\u002F\u002Ftaylorbuchanan.com","2026-11-12 04:36:15",{"source":8,"skillName":5,"commands":53,"prompt":63,"zipAvailable":64},[54,57,60],{"tool":55,"command":56},"npx","npx skills add gtbuchanan\u002Ftooling --skill gtb-eslint-config",{"tool":58,"command":59},"bunx","bunx skills add gtbuchanan\u002Ftooling --skill gtb-eslint-config",{"tool":61,"command":62},"pnpm","pnpm dlx skills add gtbuchanan\u002Ftooling --skill gtb-eslint-config","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fgtbuchanan\u002Ftooling\u002Fblob\u002Fmain\u002Fpackages\u002Feslint-config\u002Fskills\u002Fgtb-eslint-config\u002FSKILL.md",true,[66],{"path":67,"resourceType":68,"resourceTypeDesc":69,"sizeBytes":70,"sourceHtmlUrl":71},"evals\u002Fevals.json","REFERENCE","参考文档",8742,"https:\u002F\u002Fgithub.com\u002Fgtbuchanan\u002Ftooling\u002Fblob\u002Fmain\u002Fpackages\u002Feslint-config\u002Fskills\u002Fgtb-eslint-config\u002Fevals\u002Fevals.json",[73,86,95,104,113,121],{"skillNo":74,"name":75,"nameZh":6,"description":76,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":77,"categories":78,"keywords":79,"publishedAt":39},"01M6KSF1EAR45R1HTGZ7DQJXH6","three-evals","Fixture with three eval cases.","https:\u002F\u002Fgithub.com\u002Fgtbuchanan\u002Ftooling\u002Fblob\u002Fmain\u002Fpackages\u002Feslint-plugin-agent-skills\u002Ftest\u002Ffixtures\u002Fmin-evals\u002Fthree-evals\u002FSKILL.md",[],[80,81,82,83,84,85],"evals","three","three evals","eval","fixture","with",{"skillNo":87,"name":88,"nameZh":6,"description":89,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":90,"categories":91,"keywords":92,"publishedAt":39},"01M6KSEXP87SH6WEZB82Y2N6YM","one-eval","Fixture with one eval case.","https:\u002F\u002Fgithub.com\u002Fgtbuchanan\u002Ftooling\u002Fblob\u002Fmain\u002Fpackages\u002Feslint-plugin-agent-skills\u002Ftest\u002Ffixtures\u002Fmin-evals\u002Fone-eval\u002FSKILL.md",[],[83,93,85,94,84],"one eval","case",{"skillNo":96,"name":97,"nameZh":6,"description":98,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":99,"categories":100,"keywords":101,"publishedAt":39},"01M6KSEV10GHA9XX43PKN4AAFC","no-evals","Fixture without an evals\u002F directory.","https:\u002F\u002Fgithub.com\u002Fgtbuchanan\u002Ftooling\u002Fblob\u002Fmain\u002Fpackages\u002Feslint-plugin-agent-skills\u002Ftest\u002Ffixtures\u002Fmin-evals\u002Fno-evals\u002FSKILL.md",[],[102,80,103,84],"no evals","directory",{"skillNo":105,"name":106,"nameZh":6,"description":107,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":108,"categories":109,"keywords":110,"publishedAt":39},"01M6KSEQHSJKJXJVQJBS7NFP5S","empty-evals","Fixture with an empty evals array.","https:\u002F\u002Fgithub.com\u002Fgtbuchanan\u002Ftooling\u002Fblob\u002Fmain\u002Fpackages\u002Feslint-plugin-agent-skills\u002Ftest\u002Ffixtures\u002Fmin-evals\u002Fempty-evals\u002FSKILL.md",[],[80,111,112,85,84],"empty evals","empty",{"skillNo":114,"name":115,"nameZh":6,"description":116,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":117,"categories":118,"keywords":119,"publishedAt":39},"01M6KSEKTK0DM06672CEGTGP3W","valid","Fixture with a valid evals.json.","https:\u002F\u002Fgithub.com\u002Fgtbuchanan\u002Ftooling\u002Fblob\u002Fmain\u002Fpackages\u002Feslint-plugin-agent-skills\u002Ftest\u002Ffixtures\u002Fevals-schema\u002Fvalid\u002FSKILL.md",[],[115,85,84,80,120],"json",{"skillNo":122,"name":123,"nameZh":6,"description":124,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":125,"categories":126,"keywords":127,"publishedAt":39},"01M6KSEG50CQRQ071D3KJXZ16Y","non-sequential-ids","Fixture whose evals.json ids are not sequential.","https:\u002F\u002Fgithub.com\u002Fgtbuchanan\u002Ftooling\u002Fblob\u002Fmain\u002Fpackages\u002Feslint-plugin-agent-skills\u002Ftest\u002Ffixtures\u002Fevals-schema\u002Fnon-sequential-ids\u002FSKILL.md",[],[128,129,84,80,120],"ids","non sequential ids"]