ast-grep
What is HardGraph? HardGraph publishes curated, provenance-backed agent skills grounded in reproducible vendor documentation.
ast-grep (sg) searches, lints, and rewrites code structurally: matches are made
against the abstract syntax tree, so a pattern finds the same construct regardless of
formatting, whitespace, or variable names. It is polyglot — one engine, every language
Tree-sitter parses — and fast enough to run as a linter or a large-scale codemod.
The mental model is the same whether you run it once at the terminal or as a rule in CI: write a pattern that describes a syntactic shape, optionally constrain it with a rule, and either report the match or rewrite it.
Pattern syntax
A pattern is a code snippet of the target language. It matches any code with the same structure.
- Metavariables (
$NAME) match any single node:$A + $Bmatches any binary addition. - Wildcard (
$$NAME) matches one or more nodes, so it captures argument lists and statement blocks of arbitrary length. - A pattern matches a single AST node by default. To match several siblings, use the multi-statement form or write a rule that walks relations.
# Find every console.log(...) call in TypeScript/JavaScript
sg run -p 'console.log($$$)' --ts
# Find React useState with an initial value
sg run -p 'useState(0)' --lang tsx
Quotes are required when a pattern contains spaces or shell metacharacters.
Rules: from pattern to lint
A pattern matches shape; a rule adds constraints a pattern cannot express — kind of node, surrounding context, regex on text, or relationships to other nodes. Rules live in YAML and compose four ways.
| Rule kind | What it answers |
| ------------- | ------------------------------------------------------- |
| Atomic | Is this node of a given kind / pattern / regex? |
| Relational| Does this node have a specific neighbour/ancestor? |
| Composite | Do all / any / not of these sub-rules hold? |
| Utility | A named, reusable rule referenced by id elsewhere. |
# Rule: a useEffect with an empty dependency array
id: empty-effect
language: TypeScript
rule:
pattern: useEffect($$$, [])
Relational rules (inside, has, precedes, follows) are what let a rule say "a
return inside an arrow function", which no pattern alone can express.
Project configuration
sg reads sgconfig.yml (or .ast-grep config) to discover rule directories, language
defaults, and custom parsers. A rule file combines a pattern/rule with metadata: id,
severity, and a message shown on match.
# sgconfig.yml — register a directory of rule files
ruleDirs:
- ./rules
Run as a linter over the project, with --filter to select rules by id and severity to
control the message:
sg scan # lint the project with configured rules
sg scan --filter 'empty-effect'
Rewriting and transforms
Matching reports; rewriting changes. A rule carries a fix (a literal replacement,
referencing metavariables) and an optional transform block that computes new strings
from matched sub-nodes before the fix is applied. transform is what turns a mechanical
find-replace into a real codemod.
id: log-to-error
rule:
pattern: console.log($MSG)
fix: console.error($MSG)
Use sg run -p '<pattern>' --rewrite '<code>' for a one-off rewrite at the command line,
and the YAML fix/transform form for reviewed, repeatable codemods in CI.
Current vs deprecated
- The command surface is
sg— the olderast-grepalias still works butsgis the canonical binary in current releases; resolve the exact flag set from the tooling guide for the version you run. - Prefer the rule +
sg scanworkflow for anything repeatable; rawsg run -pis for exploration. Rules are versionable, testable, and share the same pattern syntax. - API bindings (Node.js, napi; Rust; Python; Go) wrap the same engine — the napi package is the maintained JavaScript integration. Check the API-usage guide for the current binding names rather than relying on memory.