Don't let your AI ship unsupervised.
Chaperone is a deterministic CLI that enforces project-specific rules that TypeScript and ESLint don't cover — things like file structure, naming conventions, required exports, file pairing, allowed patterns, and repo "invariants".
It's designed to work alongside your existing TypeScript + ESLint setup, not replace them. Initialize once, define your rules in a config file, and run it locally or in CI.
In AI-assisted workflows, code gets generated fast — but it doesn't always follow your project's conventions. Chaperone acts as your repo's "adult supervision", verifying changes comply with your rules before they're committed or merged.
TypeScript catches type errors. ESLint catches style issues. Chaperone catches everything else:
- 📁 Directory structure requirements
- 📛 File and export naming conventions
- 🔗 File pairing rules (e.g., every
.tsxneeds a.test.tsx) - 📦 Required exports per module
- 🚫 Forbidden patterns and imports
- ✅ Custom repo invariants
Chaperone ships as a single self-contained executable — no runtime, no dependencies. Pick the method that suits you.
macOS / Linux:
curl -fsSL https://raw.githubusercontent.com/marckraw/chaperone-cli/master/scripts/install.sh | shWith a specific version:
curl -fsSL https://raw.githubusercontent.com/marckraw/chaperone-cli/master/scripts/install.sh | CHAPERONE_VERSION=0.3.0 shCustom install directory:
curl -fsSL https://raw.githubusercontent.com/marckraw/chaperone-cli/master/scripts/install.sh | CHAPERONE_INSTALL_DIR="$HOME/.local/bin" shThe install script auto-detects your OS and architecture, downloads the correct binary, verifies the SHA256 checksum, and installs it. If /usr/local/bin is not writable, it falls back to ~/.local/bin.
Windows (PowerShell):
Invoke-WebRequest -Uri "https://github.com/marckraw/chaperone-cli/releases/latest/download/chaperone-windows-x64.exe" -OutFile "$env:LOCALAPPDATA\chaperone.exe"
# Add $env:LOCALAPPDATA to your PATH, or move chaperone.exe somewhere already in PATH-
Go to the latest release
-
Download the binary for your platform:
Platform Binary macOS (Apple Silicon) chaperone-darwin-arm64macOS (Intel) chaperone-darwin-x64Linux (x64) chaperone-linux-x64Linux (ARM64) chaperone-linux-arm64Windows (x64) chaperone-windows-x64.exe -
Make it executable and move to your PATH:
chmod +x chaperone-* sudo mv chaperone-* /usr/local/bin/chaperone
chaperone --version# Clone the repository
git clone https://github.com/marckraw/chaperone-cli.git
cd chaperone
# Install Bun if you haven't already
# https://bun.sh/docs/installation
# Run directly
bun run src/cli.ts --help
# Or build a standalone executable
bun run build
./bin/chaperone-darwin-arm64 --help # macOS Apple Silicon# Initialize Chaperone in your project
chaperone init
# Check your codebase against the rules
chaperone checkThe init command scans your project, detects existing tools (TypeScript, ESLint, Prettier, package manager), and creates a .chaperone.json config file.
# Initialize configuration (interactive)
chaperone init
# Initialize with defaults (non-interactive)
chaperone init --yes
# Preview config without writing
chaperone init --dry-run
# Check codebase for violations
chaperone check
# Show help
chaperone help
# Show version
chaperone versionChaperone supports custom rules in .chaperone.json under rules.custom.
{
"rules": {
"custom": []
}
}Use for forbidden/required code patterns.
{
"type": "regex",
"id": "no-console-log",
"severity": "error",
"files": "src/**/*.{ts,tsx}",
"pattern": "console\\.log\\(",
"message": "console.log is forbidden"
}Use for companion file requirements.
{
"type": "file-naming",
"id": "component-needs-test",
"severity": "error",
"pattern": "src/**/*.tsx",
"requireCompanion": {
"transform": "$1.test.tsx"
},
"message": "Each component must have a test file"
}Use for path-based file pairing where simple basename transforms are not enough.
{
"type": "file-pairing",
"id": "migration-has-validator",
"severity": "error",
"files": "src/storyblok/migrations/**/*.sb.migration.ts",
"pair": {
"from": "\\.sb\\.migration\\.ts$",
"to": ".validation.ts"
},
"mustExist": true,
"requireTransformMatch": true,
"message": "Each runnable migration must have a co-located validator"
}Use for deterministic content contracts per file, including filename-derived placeholders.
{
"type": "file-contract",
"id": "validator-id-and-name-match-file",
"severity": "error",
"files": "src/storyblok/migrations/**/*.validation.ts",
"captureFromPath": {
"pattern": "([^/]+)\\.validation\\.ts$",
"group": 1
},
"requiredPatterns": [
"defineMigrationValidation\\s*\\(",
"export\\s+default\\s+"
],
"requiredAnyPatterns": [
"ruleSet\\s*:",
"validateData\\s*:",
"validateFile\\s*:"
],
"templatedRequiredPatterns": [
"id\\s*:\\s*['\"]{{capture}}['\"]",
"name\\s*:\\s*['\"]{{capture}}['\"]"
]
}Use for package.json invariants.
{
"type": "package-fields",
"id": "require-build-script",
"severity": "error",
"requiredFields": ["scripts.build"]
}Use to keep presentational/stateful components in expected folders.
{
"type": "component-location",
"id": "presentational-in-ui",
"severity": "error",
"files": "src/**/*.tsx",
"componentType": "presentational",
"requiredLocation": "src/components/ui/**",
"mustBeIn": true
}Use to keep React components split into focused files by capping how many top-level components may exist in a single file.
{
"type": "react-component-count",
"id": "single-react-component",
"severity": "error",
"files": "src/**/*.{tsx,jsx}",
"exclude": [
"**/*.test.{tsx,jsx}",
"**/*.stories.{tsx,jsx}"
],
"maxComponents": 1,
"message": "Keep a single top-level React component in each file"
}Use for deterministic command-based checks.
{
"type": "command",
"id": "unit-tests-pass",
"severity": "error",
"command": "npm",
"args": ["run", "test:unit"],
"expectedExitCode": 0,
"message": "Unit tests must pass"
}Use to ensure exported symbols from source files are referenced in target files.
{
"type": "symbol-reference",
"id": "pure-functions-tested",
"severity": "error",
"sourceFiles": "src/**/*.pure.ts",
"targetFiles": "tests/unit/**/*.test.ts",
"symbolKinds": ["function-declaration", "function-variable"],
"message": "Exported pure functions must be referenced in unit tests"
}symbol-reference options:
symbolPattern: regex filter for symbol names.ignoreSymbols: explicit symbol names to skip.exclude: per-rule glob exclusions (same as other custom rule types).
Use to prevent files from being created in deprecated or legacy directories. Catches AI agents that create files in old locations.
{
"type": "retired-path",
"id": "no-legacy-dirs",
"severity": "error",
"paths": [
{ "pattern": "src/components/**/*", "reason": "Use layered architecture", "migratedTo": "src/features/<feature>/ui/" },
{ "pattern": "src/hooks/**/*", "migratedTo": "src/features/<feature>/model/" },
{ "pattern": "src/lib/**/*", "migratedTo": "src/shared/lib/" }
]
}Each paths entry:
pattern(required): glob for the retired location.reason: why this path is retired.migratedTo: where files should go instead.
Use to enforce content conventions on files with a specific suffix. Produces clear, named error messages instead of raw regex.
{
"type": "file-suffix-content",
"id": "presentational-purity",
"severity": "error",
"suffix": ".presentational.tsx",
"files": "src/**/*.tsx",
"forbiddenPatterns": [
{ "pattern": "\\buseEffect\\s*\\(", "name": "useEffect" },
{ "pattern": "\\buseState\\s*\\(", "name": "useState" },
{ "pattern": "\\bfetch\\s*\\(", "name": "fetch" }
],
"requiredPatterns": [
{ "pattern": "export default", "name": "default export" }
]
}Options:
suffix(required): file suffix to match (e.g.,.styles.ts,.store.ts,.presentational.tsx).files(required): glob scope.forbiddenPatterns: array of{ pattern, name }— each pattern must NOT match.requiredPatterns: array of{ pattern, name }— each pattern MUST match.
Use to enforce directory conventions like feature folder structure.
{
"type": "file-structure",
"id": "feature-folder-structure",
"severity": "error",
"parentDirs": "src/features/*",
"required": ["ui", "index.ts"],
"optional": ["lib", "model", "api", "service"],
"strict": true
}Options:
parentDirs(required): glob for parent directories to check (e.g.,src/features/*).required(required): entries that must exist in each matched directory.optional: entries that may exist.strict: iftrue, any entry not inrequiredoroptionalis a violation.
Use to restrict which files can import from specific modules or use specific patterns. Replaces complex ESLint no-restricted-imports configs.
{
"type": "forbidden-import",
"id": "tauri-api-boundary",
"severity": "error",
"files": "src/**/*.{ts,tsx}",
"restrictions": [
{
"source": "^@tauri-apps/",
"allowedIn": ["src/**/*.api.ts"],
"message": "Tauri APIs can only be used in .api.ts files"
}
],
"checkPatterns": [
{
"pattern": "\\binvoke\\s*\\(",
"allowedIn": ["src/**/*.api.ts"],
"message": "invoke() can only be used in .api.ts files"
}
],
"includeTypeImports": false
}Options:
files(required): glob for files to scan.restrictions: array of import restrictions. Each has:source(required): regex matching the import specifier.allowedIn(required): globs for files where this import IS allowed.message: custom error message.
checkPatterns: array of code pattern restrictions (same shape as restrictions but matches code, not imports).includeTypeImports: whether to checkimport typestatements (default:false— type imports are usually safe).
Use to enforce architectural layer boundaries. Prevents imports across layers that violate your dependency direction.
{
"type": "import-boundary",
"id": "fsd-layers",
"severity": "error",
"layers": {
"shared": { "files": "src/shared/**/*.{ts,tsx}", "allowImportsFrom": [] },
"entities": { "files": "src/entities/**/*.{ts,tsx}", "allowImportsFrom": ["shared"] },
"features": { "files": "src/features/**/*.{ts,tsx}", "allowImportsFrom": ["shared", "entities"] },
"widgets": { "files": "src/widgets/**/*.{ts,tsx}", "allowImportsFrom": ["shared", "entities", "features"] },
"app": { "files": "src/app/**/*.{ts,tsx}", "allowImportsFrom": ["shared", "entities", "features", "widgets"] }
}
}Options:
layers(required): map of layer name → config. Each layer has:files(required): glob for files belonging to this layer.allowImportsFrom(required): list of layer names this layer can import from.
- Self-layer imports are always implicitly allowed (a shared file can import from another shared file).
includeTypeImports: checkimport typestatements (default:true).includeDynamicImports: checkimport()expressions (default:true).
Use to enforce that modules are imported through their barrel file (index.ts), not via deep imports into internal files.
{
"type": "public-api",
"id": "feature-public-api",
"severity": "error",
"modules": "src/features/*",
"files": "src/**/*.{ts,tsx}",
"barrelFile": "index.ts",
"allowSameModule": true
}Options:
modules(required): glob for module root directories.files(required): glob for files to check.barrelFile: name of the barrel file (default:index.ts).allowSameModule: allow deep imports within the same module (default:true).
Use for composite "if A then B" rules — when a file exists, enforce conditions on the file and/or its companion.
{
"type": "relationship",
"id": "container-needs-presentational",
"severity": "error",
"when": { "files": "src/**/*.container.tsx" },
"then": [
{ "mustHaveCompanion": { "suffix": ".presentational.tsx" } },
{ "mustImport": { "companion": true } },
{ "companionMustNot": { "patterns": ["\\buseEffect", "\\buseState"] } },
{ "maxLines": 150 }
]
}The when.files glob selects trigger files. The then array is a sequence of actions:
| Action | Description |
|---|---|
mustHaveCompanion |
Companion file must exist. Use suffix (extension swap) or pair ({ from, to } regex). |
mustNotHaveCompanion |
Companion file must NOT exist. |
mustImport |
File must import companion: true or specific modules: [...]. |
mustNotImport |
File must not import specific modules: [...]. |
companionMustContain |
Companion must match patterns: [...] (regex array). |
companionMustNot |
Companion must NOT match patterns: [...]. |
fileMustContain |
Trigger file must match patterns: [...]. |
fileMustNot |
Trigger file must NOT match patterns: [...]. |
companionMaxLines |
Companion file max line count. |
maxLines |
Trigger file max line count. |
Actions are processed sequentially. If mustHaveCompanion fails (companion doesn't exist), remaining actions for that file are skipped.
The file-contract rule also supports an assertions field for semantic content checks beyond regex patterns. Assertions and patterns are additive — all must pass.
{
"type": "file-contract",
"id": "server-component-contract",
"severity": "error",
"files": "src/**/*.server.ts",
"assertions": {
"firstLine": "['\"]use server['\"]",
"mustExportDefault": true,
"mustNotImport": ["@tauri-apps/*", "react-dom"],
"maxLines": 200
}
}Available assertions:
| Assertion | Type | Description |
|---|---|---|
firstLine |
string |
First non-empty, non-comment line must match this regex. |
mustExportDefault |
boolean |
File must have export default. |
mustExportNamed |
boolean |
File must have at least one named export. |
mustNotImport |
string[] |
Module patterns that must not be imported (supports * glob). |
mustImport |
string[] |
Module patterns that must be imported. |
maxLines |
number |
Maximum line count. |
minLines |
number |
Minimum line count. |
mustHaveJSDoc |
boolean |
Exported functions must have JSDoc comments. |
maxExports |
number |
Maximum number of exports. |
mustBeModule |
boolean |
File must have at least one import or export. |
Chaperone supports shareable rule bundles via the extends field. Presets let you reuse common rule sets across projects.
{
"version": "1.0.0",
"extends": ["chaperone/react-layered"],
"rules": {
"custom": [
{
"type": "forbidden-import",
"id": "tauri-api-boundary",
"severity": "error",
"files": "src/**/*.{ts,tsx}",
"restrictions": [{ "source": "^@tauri-apps/", "allowedIn": ["src/**/*.api.ts"] }]
}
]
}
}"chaperone/<name>"— built-in preset (e.g.,"chaperone/react-layered")."./<path>"or"../<path>"— local JSON file relative to your config.
Enforces a layered React architecture with Feature-Sliced Design conventions:
- Import boundaries between shared → entities → features → widgets → app layers.
- Retired paths for
src/components/,src/hooks/,src/lib/(legacy flat structure). - Presentational purity —
.presentational.tsxfiles cannot useuseEffect,useState,useContext, orfetch. - Pure file purity —
.pure.tsfiles cannot use side effects. - Pure file testing —
.pure.tsfiles must have paired.pure.test.tsfiles. - Public API enforcement — feature modules must be imported through their
index.tsbarrel file.
Enforces one top-level React component per *.tsx / *.jsx file, excluding test and story files by default.
User config rules override preset rules with the same id. To disable a preset rule:
{
"extends": ["chaperone/react-layered"],
"rules": {
"custom": [
{ "type": "retired-path", "id": "preset/no-legacy-dirs", "severity": "error", "disabled": true, "paths": [] }
]
}
}All custom rules share these base fields:
| Field | Type | Required | Description |
|---|---|---|---|
type |
string |
Yes | Rule type (see sections above). |
id |
string |
Yes | Unique identifier for this rule. |
severity |
"error" | "warning" |
Yes | Severity level. Errors cause non-zero exit code. |
exclude |
string[] |
No | Glob patterns to exclude from this rule. |
disabled |
boolean |
No | Set to true to disable a preset rule. |
message |
string |
No | Custom error message (most rule types). |
Add Chaperone to your CI pipeline to catch convention violations before merge:
# GitHub Actions example
- name: Run Chaperone
run: chaperone checkThis repository uses Changesets for version control and GitHub Actions for binary releases.
Target branch: master
For user-facing/code changes, include a changeset:
bun run changesetThe PR check (Changeset Check) enforces this for changes under src/, build.ts, or package.json.
On pushes to master, Changeset Version PR runs and opens/updates a version PR using changesets/action.
After version bumps land on master, Release Binaries:
- reads
package.jsonversion, - creates
v<version>tag if missing, - builds Bun executables for all targets,
- generates
SHA256SUMS.txt, - publishes assets to GitHub Releases.
Release assets include:
chaperone-darwin-arm64chaperone-darwin-x64chaperone-linux-x64chaperone-linux-arm64chaperone-windows-x64.exe
# Run in development mode
bun run dev
# Build for current platform
bun run build
# Build for all platforms
bun run build:allChaperone uses Bun's compile feature to create standalone executables:
# Build for current platform only
bun run build.ts
# Build for all supported platforms
bun run build.ts --allSupported platforms:
- macOS (Apple Silicon):
chaperone-darwin-arm64 - macOS (Intel):
chaperone-darwin-x64 - Linux (x64):
chaperone-linux-x64 - Linux (ARM64):
chaperone-linux-arm64 - Windows (x64):
chaperone-windows-x64.exe
MIT