No description
Find a file
github-actions[bot] 9a13181b9f
chore: release v0.11.0 (#49)
## 🤖 New release

* `ifttt-lint`: 0.10.8 -> 0.11.0 (⚠ API breaking changes)

### ⚠ `ifttt-lint` breaking changes

```text
--- failure enum_missing: pub enum removed or renamed ---

Description:
A publicly-visible enum cannot be imported by its prior path. A `pub use` may have been removed, or the enum itself may have been renamed or removed entirely.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/enum_missing.ron

Failed in:
  enum ifttt_lint::vcs::FilePattern, previously in file /tmp/.tmpZATvpI/ifttt-lint/src/vcs.rs:56

--- failure struct_missing: pub struct removed or renamed ---

Description:
A publicly-visible struct cannot be imported by its prior path. A `pub use` may have been removed, or the struct itself may have been renamed or removed entirely.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/struct_missing.ron

Failed in:
  struct ifttt_lint::vcs::FileFilter, previously in file /tmp/.tmpZATvpI/ifttt-lint/src/vcs.rs:79

--- failure trait_method_added: pub trait method added ---

Description:
A non-sealed public trait added a new method without a default implementation, which breaks downstream implementations of the trait
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#trait-new-item-no-default
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/trait_method_added.ron

Failed in:
  trait method ifttt_lint::vcs::VcsProvider::read_file_bytes in file /tmp/.tmpEPKptF/ifttt-lint/src/vcs.rs:73

--- failure trait_method_parameter_count_changed: pub trait method parameter count changed ---

Description:
A trait method now takes a different number of parameters.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#trait-item-signature
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/trait_method_parameter_count_changed.ron

Failed in:
  VcsProvider::search_string_in_files now takes 1 instead of 2 parameters, in file /tmp/.tmpEPKptF/ifttt-lint/src/vcs.rs:87
```

<details><summary><i><b>Changelog</b></i></summary><p>



</p></details>

---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-30 22:57:08 +02:00
.agents feat: implement ifttt-lint 2026-03-15 00:00:00 +01:00
.claude feat: implement ifttt-lint 2026-03-15 00:00:00 +01:00
.github/workflows ci: switch CodSpeed to simulation mode (#31) 2026-05-29 12:29:57 +02:00
assets feat: implement ifttt-lint 2026-03-15 00:00:00 +01:00
src fix(pre-commit): pin the git backend in hook entries (#51) 2026-07-30 22:47:25 +02:00
tests feat: implement ifttt-lint 2026-03-15 00:00:00 +01:00
.gitignore feat: implement ifttt-lint 2026-03-15 00:00:00 +01:00
.oxfmtrc.json feat: implement ifttt-lint 2026-03-15 00:00:00 +01:00
.pre-commit-config.yaml feat: implement ifttt-lint 2026-03-15 00:00:00 +01:00
.pre-commit-hooks.yaml fix(pre-commit): pin the git backend in hook entries (#51) 2026-07-30 22:47:25 +02:00
action.yml feat: implement ifttt-lint 2026-03-15 00:00:00 +01:00
AGENTS.md feat: implement ifttt-lint 2026-03-15 00:00:00 +01:00
Cargo.lock chore: release v0.11.0 (#49) 2026-07-30 22:57:08 +02:00
Cargo.toml chore: release v0.11.0 (#49) 2026-07-30 22:57:08 +02:00
CLAUDE.md feat: implement ifttt-lint 2026-03-15 00:00:00 +01:00
context7.json feat: implement ifttt-lint 2026-03-15 00:00:00 +01:00
deny.toml feat: implement ifttt-lint 2026-03-15 00:00:00 +01:00
license feat: implement ifttt-lint 2026-03-15 00:00:00 +01:00
mise.toml feat(jj): add Jujutsu VCS backend (#32) 2026-05-29 14:14:47 +02:00
readme.md chore: release v0.11.0 (#49) 2026-07-30 22:57:08 +02:00
release-plz.toml feat: implement ifttt-lint 2026-03-15 00:00:00 +01:00

ifttt-lint — stop cross-file drift with Google's IfChange/ThenChange comments

Stop cross-file drift with Google's IfChange / ThenChange comments.
Open-source reimplementation of Google's internal IfThisThenThat linter.

crates.io license

The Problem

You add a field to a Go struct and forget the TypeScript mirror. You bump a constant and forget the docs. You rename a database column and forget the migration. You only discover it when something breaks in production — or worse, when a user reports it weeks later.

ifttt-lint is built to catch exactly that. You wrap co-dependent sections in LINT.IfChange / LINT.ThenChange comment directives. When a diff touches one side but not the other, the tool fails — before the change reaches production. The model is intentionally simple, which keeps it predictable.

This repo dogfoods its own directives to keep the tool version in sync across Cargo.toml, the pre-commit config, and the CI release pipeline. Automation (release-plz) does the normal sync; the directives catch the case where someone edits one of those places manually and bypasses the release flow.

Setup

GitHub Actions

on:
  push:
    branches: [main]
  pull_request:

jobs:
  ifttt-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: simonepri/ifttt-lint@v0.11.0

The action mirrors the two hooks:

  • pull_request — diff validation equivalent to ifttt-lint-diff. Validates co-changes across all commits in the PR. Supports NO_IFTTT suppression via commit messages.
  • push — structural validation on all tracked files, equivalent to ifttt-lint '**/*'. Use on.push.branches to control which branches run it.
- repo: https://github.com/simonepri/ifttt-lint
  rev: v0.11.0
  hooks:
    - id: ifttt-lint
    - id: ifttt-lint-diff

Two hooks serve different purposes:

  • ifttt-lint — runs at every commit on the staged files. Checks that all ThenChange targets and labels exist on disk, directives are properly paired, and syntax is valid. Also supports pre-commit run --all-files for full-repo structural scans.

  • ifttt-lint-diff — runs at every push on all files in the diff range. Checks that co-dependent files are updated together. Supports NO_IFTTT suppression via commit messages. Mirrors the pull_request GitHub Actions check in intent: diff-based validation with the same suppression mechanism, though the exact git range differs by context.

Both hooks pin --vcs git: git hooks only ever fire from git and pass git refs, so in a Jujutsu colocated repo backend auto-detection would otherwise hand those refs to jj as revsets and fail.

Install the CLI manually

If you prefer running ifttt-lint directly, install it with Cargo:

cargo install ifttt-lint

See the CLI reference below for invocation patterns and flags.

Using with coding agents

Guidance for AGENTS.md sits on top of any of the install paths above — it doesn't replace them. If you use coding agents (Codex, Claude Code, etc.), consider adding something like this to your AGENTS.md so agents create directives proactively and keep them narrow:

#### Co-dependent changes: use IFTTT directives

When code in one place must stay in sync with code elsewhere — but DRY cannot eliminate the duplication (for example, cross-language boundaries, config mirroring code, or encode/decode pairs) — mark the dependency with `LINT.IfChange` / `LINT.ThenChange` directives so changes to one side prompt review of the other. Add these directives proactively when creating new co-dependent content, not just when maintaining existing pairs.

Keep the guarded block as small as possible. Prefer several small labeled source->target pairs over one large catch-all block unless the whole region genuinely needs to change together. The directives can be validated and enforced via [ifttt-lint](https://github.com/simonepri/ifttt-lint/blob/main/readme.md).

<details>
<summary>Example</summary>

```javascript
// LINT.IfChange(speed_threshold)
SPEED_THRESHOLD_MPH = 88;
// LINT.ThenChange(
//     //db/migrations/temporal_displacement.sql,
//     //docs/delorean.md:speed_threshold,
// )
```

</details>

Usage

Add directives as comments in any supported language — the tool auto-detects comment styles based on file extension.

Keep code and docs in sync

Your upload limit is defined in code and referenced in the API docs. Label both sides and link them — if one changes, the other must too:

config/upload.py docs/api.md
# LINT.IfChange(upload_limit)
MAX_UPLOAD_SIZE_MB = 50
# LINT.ThenChange(//docs/api.md:upload_limit)
<!-- LINT.IfChange(upload_limit) -->
Files up to 50 MB are accepted.
<!-- LINT.ThenChange(//config/upload.py:upload_limit) -->

Bump the limit to 100 MB but forget the docs? The linter catches it:

config/upload.py:1: warning: changes in this block may need to be reflected in docs/api.md:upload_limit

Sync across language boundaries

When types cross language boundaries, a shared schema language (Protocol Buffers, Thrift, GraphQL) is the best solution. But not every project uses one — and even when it does, hand-written types often exist alongside generated ones. For those cases, link the two sides directly:

api/types.go web/src/types.ts
// LINT.IfChange(user_response)
type UserResponse struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}
// LINT.ThenChange(//web/src/types.ts:user_response)
// LINT.IfChange(user_response)
interface UserResponse {
  id: string;
  name: string;
  email: string;
}
// LINT.ThenChange(//api/types.go:user_response)

A rate limit touches the database, the docs, and an alerting threshold in the same file. List all dependents — the tool checks every target:

# LINT.IfChange
RATE_LIMIT_RPS = 100
# LINT.ThenChange(
#     //db/migrations/002_rate_limits.sql,
#     //docs/api.md:rate_limits,
#     :alert_threshold,
# )

Sync within a file

Serialize and deserialize must stay in lockstep — use :label to reference another section in the same file:

# LINT.IfChange(serialize_event)
def serialize_event(event: Event) -> bytes: ...
# LINT.ThenChange(:deserialize_event)

# LINT.IfChange(deserialize_event)
def deserialize_event(data: bytes) -> Event: ...
# LINT.ThenChange(:serialize_event)

Reference

Directive syntax

ifttt-lint implements Google's LINT.IfChange / LINT.ThenChange directive syntax†.

Directive Description
LINT.IfChange Marks the start of a watched region
LINT.IfChange(label) Watched region with a named label (targetable from other files)
LINT.ThenChange(//path) End of watched region; requires target file to be modified
LINT.ThenChange(//path:label) Requires changes within a specific label range in the target
LINT.ThenChange(:label) Same-file label reference
LINT.ThenChange(//a, //b) Multiple targets (comma-separated)

† ifttt-lint enforces stricter path rules than Google's internal linter by default — use --strict=false for Google-compatible behavior.

Path rules

  • All file paths must start with // (project-root-relative)
  • : separates file path from label (splits on last :) — Windows drive-letter colons (e.g. C:\) are not treated as label separators
  • :label alone means same-file reference

Use --strict=false for Google-compatible behavior — bare paths (path/to/file), single-/ paths (/path/to/file), and explicit same-file path references (//same-file.h:label instead of :label) are all accepted without warnings.

Label format

Labels must start with a letter, followed by letters, digits, underscores, dashes, or dots. For example: upload_limit, user-response, section2, Payments.Pix.Result.

Exact matching

Directives must appear alone on their comment line — no extra text before or after the directive pattern. If a comment contains a directive-like pattern with trailing text (e.g. // LINT.IfChange(label) see docs or // LINT.ThenChange(//path) -->), it is silently ignored as prose. This avoids false positives from documentation or comments that mention directive syntax without intending to create a directive.

Nesting

Nesting IfChange blocks is not supported — each IfChange must be closed by a ThenChange before another can begin. A consecutive IfChange without an intervening ThenChange is reported as an error. Nesting could be added (stack-based pairing) but lacks a clear use case: any nested scenario decomposes into sequential blocks with multiple targets.

Comment syntax

Directives use the supported comment syntax for each file extension — //, #, <!-- -->, --, ;, %, and /* */ all work depending on the language. See supported languages for the full registry.

-- LINT.IfChange(schema)
CREATE TABLE users (id UUID, name TEXT, email TEXT);
-- LINT.ThenChange(//api/types.go:user_response)
# LINT.IfChange(deploy_config)
replicas: 3
# LINT.ThenChange(//docs/runbook.md:scaling)

Fenced code blocks

Directives inside fenced Markdown code blocks (```) are ignored — the linter won't fire on examples in documentation. This README itself contains dozens of LINT.IfChange examples and passes ifttt-lint cleanly. The same applies to code blocks inside doc comments (Rust ///, Python docstrings with embedded examples).

CLI reference

ifttt-lint [OPTIONS] [FILES]...
Argument Description
FILES... Files to validate structurally: checks that every ThenChange target and label exists on disk, regardless of whether the file was modified. Supports glob patterns resolved internally via git ls-files to avoid shell ARG_MAX limits. * matches within a single directory level; use **/* for recursive matching (e.g. '**/*.rs').
Option Description
-d, --diff <RANGE> VCS ref range / revset to diff (e.g. main...HEAD for git, main..@ for jj)
-t, --threads <N> Worker thread count (default: 2; 0 = same as 2)
-i, --ignore <PATTERN> Permanently ignore target pattern, repeatable (glob syntax)
--strict=false Accept bare and single-/ paths in ThenChange targets (in addition to //). Required for codebases that use Google-internal path conventions
-f, --format <FMT> Output format: pretty (default), json, plain
--vcs <KIND> VCS backend: git or jj. Auto-detected from .jj/ / .git/ presence if omitted (see Supported backends)

The --diff value is interpreted by the selected backend, so its syntax must match: git refs/ranges for git, jj revsets for jj. Auto-detection prefers jj in colocated repos; pass --vcs git to force the git backend.

Exit Code Meaning
0 No errors
1 Lint errors found
2 Fatal error (bad diff, I/O failure)

Supported backends

Backend Notes
Git Uses git diff, git log, git grep, git ls-files. Requires git on PATH.
Jujutsu (jj) Uses jj diff --git, jj log, jj file list. Multi-pattern fixed-string search runs in-process via aho-corasick. Requires jj on PATH.

Auto-detected from .jj/ / .git/ directory presence in the current working directory or any ancestor (.jj/ wins on colocated repos). Override with --vcs=git|jj. In a colocated repo, git-ref ranges like origin/main..HEAD fail as jj revsets — pass --vcs git to use them.

Need a different backend (Mercurial, Perforce, …)? Open an issue. #32 is a worked example of adding one.

Validation

ifttt-lint runs up to three validation passes depending on how it's invoked. The default pretty output format uses standard file:line: severity: message syntax, compatible with most editors and CI systems.

CLI modes

Invocation Hook stage What runs
ifttt-lint (no args) — Nothing — exits 0 with a hint
ifttt-lint FILES… (no --diff) pre-commit Structural validation on listed files only
ifttt-lint '**/*' (no --diff) CI, manual Structural validation on all tracked files (glob expanded via git ls-files)
ifttt-lint --diff REF FILES… — Structural validation on listed files. Diff validation scoped to listed files. Reverse lookup for deleted files and stale labels
ifttt-lint --diff REF (no files) pre-push Structural + diff validation on all files in the diff. Reverse lookup for deleted files and stale labels

Diff-based validation

When an IfChange…ThenChange block is present in a changed file, the tool checks whether the guarded content (lines between the directives) was modified. If it was, every ThenChange target must also show changes in the same diff — otherwise a finding is reported.

Changes to the directive lines themselves (adding a new pair, renaming a label, adding or removing a ThenChange target) do not trigger validation — only content between the directives matters. The tool does not try to infer semantic code moves; it validates the file, label, and path coordinates visible in the diff.

Fires:

A field is added to the Go struct but the TypeScript mirror is not updated:

  // LINT.IfChange(user_response)
  type UserResponse struct {
      ID    string `json:"id"`
      Name  string `json:"name"`
+     Avatar string `json:"avatar"`
  }
  // LINT.ThenChange(//web/src/types.ts:user_response)
api/types.go:1: warning: changes in this block may need to be reflected in web/src/types.ts:user_response

Content is modified and a new target is added in the same diff — all targets (including the new one) must reflect the change, because you're declaring a dependency while simultaneously changing the content it guards:

  // LINT.IfChange(upload_limit)
- MAX_UPLOAD_SIZE_MB = 50
+ MAX_UPLOAD_SIZE_MB = 100
- // LINT.ThenChange(//docs/api.md:upload_limit)
+ // LINT.ThenChange(
+ //     //docs/api.md:upload_limit,
+ //     //alerts/thresholds.yaml:upload_limit,
+ // )
config/upload.py:1: warning: changes in this block may need to be reflected in docs/api.md:upload_limit
config/upload.py:1: warning: changes in this block may need to be reflected in alerts/thresholds.yaml:upload_limit

Does not fire:

Adding a new directive pair — the directive is being established, not the content changed. This holds even when the guarded content is also new or modified in the same diff: the coupling didn't exist before, so nothing can be out of sync yet. A pair counts as new when both its IfChange and ThenChange lines are added (a label rename leaves the ThenChange line as context, so renaming still validates content):

+ // LINT.IfChange(speed_threshold)
- SPEED_LIMIT_MPH = 65
+ SPEED_THRESHOLD_MPH = 88
+ // LINT.ThenChange(//docs/delorean.md:speed_threshold)

Adding a new target to an existing directive — directive metadata changed, not guarded content:

  // LINT.IfChange(rate_limit)
  RATE_LIMIT_RPS = 100
- // LINT.ThenChange(//docs/api.md:rate_limits)
+ // LINT.ThenChange(
+ //     //docs/api.md:rate_limits,
+ //     //alerts/thresholds.yaml:rate_limits,
+ // )

Renaming a label — directive metadata changed; stale references are caught by the reverse lookup instead:

- // LINT.IfChange(old_name)
+ // LINT.IfChange(new_name)
  SPEED_THRESHOLD_MPH = 88
  // LINT.ThenChange(//docs/delorean.md:speed_threshold)

Label renamed and content changed in the same commit — a known edge case. The tool sees this as a full delete + add (the whole IfChange(old_name) block disappears, a new IfChange(new_name) block appears), so the content change is not flagged for diff-based validation:

- // LINT.IfChange(old_name)
- MAX_UPLOAD_SIZE_MB = 50
+ // LINT.IfChange(new_name)
+ MAX_UPLOAD_SIZE_MB = 100
  // LINT.ThenChange(//docs/api.md:upload_limit)

If you need both a rename and a content change, split them across two commits so the second one triggers the sync check. Stale references to the old label are still caught by reverse lookup in the meantime.

Both sides updated in the same diff — the target already reflects the change:

  // LINT.IfChange(upload_limit)
- MAX_UPLOAD_SIZE_MB = 50
+ MAX_UPLOAD_SIZE_MB = 100
  // LINT.ThenChange(//docs/api.md:upload_limit)
  <!-- LINT.IfChange(upload_limit) -->
- Files up to 50 MB are accepted.
+ Files up to 100 MB are accepted.
  <!-- LINT.ThenChange(//config/upload.py:upload_limit) -->

Suppressed via NO_IFTTT in the commit message — explicitly opted out (see Suppression below).

Structural validation

When files are passed as positional arguments (FILES…), the tool checks directive structure regardless of the diff. This catches issues that diff-based validation can't see — broken references, missing targets, malformed syntax.

Check Example message
ThenChange target file doesn't exist target file not found: web/src/old_types.ts
ThenChange label not found in target label upload_limit not found in docs/api.md
IfChange without matching ThenChange LINT.IfChange without matching ThenChange
ThenChange without preceding IfChange LINT.ThenChange without preceding IfChange
Duplicate IfChange labels in same file duplicate LINT.IfChange label foo

Reverse lookup

Reverse lookup covers stale references that diff-based validation cannot see:

  • When a target file is deleted or renamed, surviving ThenChange(//old/path) references are reported as stale old paths.
  • When a file is modified and an IfChange label disappears from that file's current contents, surviving ThenChange(//path:old_label) references are reported as stale old labels. This includes label renames, removals, and moving the labeled block elsewhere, but it is still based on the old file+label coordinate rather than semantic move tracking.
api/types.go:7: warning: target file not found: web/src/old_types.ts
config/upload.py:3: warning: label old_name not found in constants.py

Reverse lookup always runs globally — it is not scoped by the file list.

Suppression

When you intentionally skip diff-based ThenChange checks for a commit, add NO_IFTTT=<reason> to the commit message:

feat: raise upload limit to 100 MB

NO_IFTTT=docs will be updated in a follow-up

NO_IFTTT=<reason> in any commit message in the scanned range suppresses diff-based validation for the entire range. Structural validation and deleted-file reverse lookup always run regardless of NO_IFTTT. The tag has no effect without --diff.

Scope — each context scans exactly one range:

Context Diff range Commit messages scanned
pre-push hook FROM_REF..TO_REF (all unpushed commits) All unpushed commits
Pull request (CI) BASE_SHA...HEAD_SHA (merge-base to PR head) All commits in the PR
Push to main (squash merge, CI) BEFORE..HEAD (1 commit) That squashed commit
Push to main (rebase merge, N, CI) BEFORE..HEAD (all N commits) All N commits
Push to main (merge commit, CI) BEFORE..HEAD (merge + PR branch commits) Merge commit and all PR branch commits

To permanently ignore targets, use --ignore:

ifttt-lint --ignore "generated/**" --ignore "*.lock"

Supported languages

Comment style is detected by file extension. The full language registry with skip-pattern documentation lives in src/languages.rs — 44 entries covering many common file extensions.

Style Languages
// /* C/C++, C#, Dart, Go, Groovy, Java, JavaScript, Kotlin, Objective-C†, Protobuf, Rust, Scala, SCSS, Swift, TypeScript
# CMake, Dockerfile, Elixir, GN, GraphQL, Makefile, Nix, Perl, PowerShell, Python, R, Ruby, Shell, Starlark, Terraform, TOML, YAML
<!-- --> HTML, Markdown, XML
-- Haskell, Lua, SQL
; Lisp / Clojure
% LaTeX
/* */ CSS
{{/* */}} Go Template / Helm (.tpl, .tmpl, .gotmpl, .gohtml)

Multi-syntax: Vue/Svelte (//, /*, <!--), PHP (//, /*, #), Terraform (#, //, /*).

Unknown extensions fall back to //, /*, #.

† .m files are shared by Objective-C (//) and MATLAB (%). Both comment prefixes are accepted so directives work in either language.

Note: Directives are recognized only on single comment lines — either line comments (//, #, etc.) or block comments used on a single line (/* ... */, <!-- ... -->). Multi-line block comments spanning several lines are not scanned, which avoids false positives from commented-out blocks.

FAQ

How does this relate to Google's internal linter?

The LINT.IfChange / LINT.ThenChange pattern originated inside Google and is documented publicly in Chromium's developer guide; Chromium, TensorFlow, Fuchsia, and other Google codebases use it at scale (Chromium alone has well over a thousand LINT.IfChange directives in tree). This repo is an independent open-source reimplementation — not affiliated with or endorsed by Google — that follows the same directive syntax and semantics, so existing Google guidance applies, but shares no code with Google's internal tooling. Some edge behavior differs (e.g. stricter path rules by default); use --strict=false for Google-compatible behavior.

Why not use types, codegen, or a shared schema?

When you can, you absolutely should. IfChange/ThenChange is for the gaps that remain — code ↔ prose docs, code ↔ config, hand-written types alongside generated ones, encode/decode pairs — where a shared schema either doesn't cover the boundary or costs more to introduce than the duplication it removes.

Is this fast enough for large codebases?

Yes. ifttt-lint is designed so the common case stays local and the worst case stays filtered — the linter reads only the changed files plus referenced targets. When a target file is deleted or a label is renamed, a single needle-only git grep (respecting .gitignore) lists the directive-containing files, and the few hits are matched against the changed paths in memory — so runtime stays flat no matter how many files the diff touches.

Real-world (structural validation, M-series MacBook, 2 threads):

Repository Tracked files Files with directives Time
Chromium 488k (~3.9GB) 1.7k (~39MB) 0.9 s
TensorFlow 36k (~402MB) 244 (~5.3MB) 0.2 s

Default thread count is 2 (--threads 0); higher counts hit filesystem I/O contention and don't help. Reproduce with cargo smoke.

Can I use this in a monorepo with multiple languages?

Yes — that's the primary use case. Directives work across any file types in the supported languages table. Paths are project-root-relative (//), so they work regardless of where the files live in the tree.

Does it work across repositories?

No — paths are project-root-relative (//), so all linked files must live in the same repository. Cross-repo dependencies are a fundamentally harder problem (versioning, release cadence, ownership boundaries) that a comment directive can't solve. If you need cross-repo coordination, consider shared packages with versioned contracts, or a schema registry. If you have ideas on how cross-repo support could work, open an issue.

My language isn't in the supported list — can I add it?

Yes, please contribute! For many languages, adding support is just a new entry in the comment-style table. Some languages may need a new skip pattern if their string or comment syntax is unusual. PRs welcome; open an issue if you're unsure about the comment syntax.

Are there other implementations?

if-changed, ifttt-lint, and ifchange exist but use different syntax and/or aren't validated on large-scale repos. For background on the pattern, see IfChange/ThenChange, Syncing Code, and Fuchsia presubmit checks.