Skip to content
Foundry
Esc
↑↓navigate↵open⌘Jpreview
On this page

Linting

ESLint, Oxlint, and Biome rules that check what a prompt references, as you write it.

@foundry/linting checks prompts without running the code. The linter parses the file and walks it; the rules pick out the prompts, hand their text to @foundry/prompts, and report each finding where it was written. One plugin loads in ESLint and in Oxlint, and a generated GritQL file does the name-free half of the work in Biome. The editor extension for any of them shows the findings inline.

The package is the home for every Foundry lint rule, grouped by domain. Prompts are the first domain; its rules are foundry/prompts/<code>.

A TypeScript file with four findings underlined inside prompts tags, and the Problems panel listing each with its rule

ESLint’s VS Code extension on Claude Code prompts, in 2026 Dark: an unknown skill, a Codex sigil, $ARGUMENTS in a message, and a built-in command.

The same TypeScript file and Problems panel in a light theme

The same file in 2026 Light.

What is checked

The rules read tagged templates and calls on a member named after a stock dialect, on any object: prompts.claude, mine.codex("…"), x.pi. A call counts when its only argument is a string literal or a template literal with no substitutions. A ${} inside a tagged template is a hole, and nothing is ever reported across one: a reference built at runtime is dynamic, not unknown.

Rule Reports Biome
foundry/prompts/unresolved-reference A skill or agent that nothing listed has, and a command or variable the harness does not know.
foundry/prompts/foreign-notation A skill or agent you have, written the way another harness reads it: $review in a Claude Code prompt, where it should be /review.
foundry/prompts/template-only $ARGUMENTS, ${CLAUDE_SKILL_DIR}, !`cmd` and the like in a message, where the harness sends them as literal text. yes
foundry/prompts/consumed-by-harness A message the harness acts on itself, such as /clear or !git status, so the model never receives it. yes

Each diagnostic code is its own rule, named after the code, so each has its own severity. The two codes about interpolated values, misread-reference and unsupported-reference, need the value and are left to the tag at runtime.

The first two rules need to know what exists. Until you list skills or agents, no skill or agent is questioned and no sigil is called foreign. Commands and harness variables are checked against each dialect’s built-ins regardless, so prompts.codex opening with /nope is reported with no configuration at all.

Setup

The package is internal to Foundry for now, like @foundry/prompts. Point it at your skills and agents; the settings under settings.foundry.prompts are shared by every prompts rule.

// eslint.config.js
import tsParser from "@typescript-eslint/parser";
import foundry from "@foundry/linting";

export default [
  {
    files: ["**/*.ts"],
    languageOptions: { parser: tsParser },
    ...foundry.configs.recommended,
    settings: {
      foundry: {
        prompts: {
          skills: [".claude/skills", "plugin:lint"],
          agents: [".claude/agents"],
        },
      },
    },
  },
];
// .oxlintrc.json
{
  "jsPlugins": ["@foundry/linting"],
  "rules": {
    "foundry/prompts/unresolved-reference": "error",
    "foundry/prompts/foreign-notation": "error",
    "foundry/prompts/template-only": "warn",
    "foundry/prompts/consumed-by-harness": "warn"
  },
  "settings": {
    "foundry": {
      "prompts": { "skills": [".claude/skills"], "agents": [".claude/agents"] }
    }
  }
}
Setting Meaning
skills, agents Names, or directories to list. An entry with a slash or a leading dot is a directory. A skills directory contributes its subdirectory names, as in .claude/skills/<name>/SKILL.md; an agents directory contributes its .md file names. Paths are relative to the linter’s working directory.
dialects Extra member names and the stock dialect each reads as: { "house": "claude" } checks mine.house from assemble.
surface message by default. Set template for code that writes skill and command files, where $ARGUMENTS and inline shell belong.

Nothing is discovered beyond what is listed. A directory is read with one readdir and the listing is kept for two seconds, so a new skill shows up in the editor without a read per prompt.

configs.recommended is an ESLint flat config object with every rule on, errors as errors and warnings as warnings. Oxlint’s JS plugins are alpha; the plugin’s tests run oxlint over the sample files to prove it loads.

Biome

Biome’s plugins are GritQL patterns, which cannot call @foundry/prompts. So the package generates biome/prompts-*.grit from a regex mirror of the notations, the way the editor grammar mirrors them, and tests that Biome reports where ESLint reports.

// biome.jsonc
{
  "plugins": [
    "./node_modules/@foundry/linting/biome/prompts-consumed-by-harness.grit",
    "./node_modules/@foundry/linting/biome/prompts-template-only.grit"
  ]
}

Biome covers the two checks that need no list of names. It reads only the stock member names, always as a message, and underlines the whole prompt once rather than each reference, since a GritQL regex matches a node’s whole text. The rule name is at the end of the message; every plugin finding is in Biome’s plugin category, so suppress one with biome-ignore lint/plugin. The other two rules wait for Biome to read directories or run JavaScript.

In the editor

The ESLint, Oxc, and Biome extensions for VS Code read the same config and underline each finding, with the message in the Problems panel. Nothing in the Foundry Prompts extension runs for this: it colours references, and the linter checks them.

Limits

  • References by value. ${ref.skill("x")} and a defineSkill value are holes to a static rule. The tag checks them at runtime; a type-aware check is a later step.
  • Skill and agent files. SKILL.md and .claude/agents/*.md are not linted. Biome has no Markdown parser yet (biome#3718); the plan is to wait for it rather than lint Markdown through ESLint alone.
  • Biome checks two of the four. Resolution and foreign notation need names, which a GritQL file cannot read. That changes if Biome ships JS plugins, at which point the ESLint rules port directly.

Was this page helpful?