@alexcodeplace/slopgate
v0.3.3
Published
Global code-quality / anti-slop gate — engine shared, rules per-project.
Readme
slopgate
A global code-quality / anti-slop gate for Claude Code and git. Engine is shared, rules are per-project.
What it does: Catches code quality violations in two tiers — a fast post-edit scan (regex + AST rules, instant feedback) and a heavy commit-tier scan (static type checkers, dead-code analysis, architecture rules, copy-paste detection). A ratchet baseline lets legacy repos adopt without flooding — only NEW violations block commits; pre-existing ones are baselined and tracked for paydown.
Features
Two-tier gate
- Fast tier (post-edit hook): regex patterns + AST rules, instant feedback as you code
- Commit tier (pre-commit hook): includes heavy checkers (tsc, knip, jscpd, dependency-cruiser, type-coverage, diff-shape) + AST + regex, blocks commits
Ratchet baseline — snapshot violations at adoption time; only NEW violations fail the gate. Track debt paydown over time.
Six commit-tier checkers
- tsc — TypeScript type errors (full-project scope)
- knip — dead/unused code (exports, files, dependencies)
- jscpd — copy-paste duplication (token-level)
- dependency-cruiser — architecture rules (cycles, orphans, layer boundaries)
- type-coverage — propagation of
anytype (per-expression tracking) - diff-shape — wide commits spanning too many directories (encourages focused changes)
Shared regex + AST rule packs — fast-tier and commit-tier both run these
- Convention:
no-stubs,ts-suppress,as-any,no-narration-comments,raw-hex(design tokens),sql-safety - Security:
live-secrets,eval-ban,pii-logs,weak-hash - Cloudflare boundary:
kv-ban(plus the opt-instack = ["cloudflare"]pack) - Built-in AST rules: empty-catch, unsafe
innerHTML/dangerouslySetInnerHTML,target="_blank"withoutrel,windowaccess during render
- Convention:
Native git pre-commit hook — no daemon, no CI coupling, just git
Claude Code integration — hooks into PreToolUse (commit) and PostToolUse (edit) events
Suppressions — per-file, per-line, with line-hash stability across edits
Self-test —
slopgate --self-testvalidates rule engines + baseline checker parsers against bundled fixtures
Install
npm install -g slopgateThe matching prebuilt native engine for your platform (linux / macOS / Windows × x64 / arm64) is pulled in automatically as an optional dependency — no toolchain or build step required.
Then onboard a project:
slopgate init [path-to-repo]This:
- Detects TypeScript roots, file extensions, and package layout
- Scaffolds
.slopgate/config.tomlwith detected checkers enabled - Writes
.slopgate/suppressions.jsonand.slopgate/depcruise.cjs(starter) - Creates
.slopgate/convention-sources.json(hints for authoring project rules from local skills/agents/docs) - Creates
.slopgate/rules/ast/and.slopgate/fixtures/src/directories - Installs git pre-commit hook (or appends to existing)
- Merges Claude Code hook settings into
.claude/settings.json - Prints next steps (including: run
slopgate baseline --config .slopgate/config.toml)
Quickstart
Run the gate on staged changes (pre-commit):
slopgate --staged --config .slopgate/config.tomlRun on a single file (post-edit, fast tier):
slopgate --file src/app.ts --config .slopgate/config.tomlCreate/update the baseline:
# Create baseline (refuses if it exists)
slopgate baseline --config .slopgate/config.toml
# Update baseline (re-snapshot all current violations)
slopgate baseline --update --config .slopgate/config.toml
# Prune baseline (remove entries no longer occurring)
slopgate baseline --prune --config .slopgate/config.tomlRun self-test (validate the engine against bundled fixtures):
slopgate --self-test --config "$(npm root -g)/slopgate/rules/baseline/selftest.config.toml"Run immutable full-repository CI gate:
slopgate scan --scope repo --tier commit --format github --config .slopgate/config.tomlRecord reviewer defects and enforce rule harvesting:
slopgate defect record --class missing-button-type --file src/app.tsx --line 42 --source code-review --config .slopgate/config.toml
slopgate harvest --check --config .slopgate/config.tomlSecond distinct occurrence requires .slopgate/rules/ast/<class>.yml plus .slopgate/fixtures/<class>.invalid.ts[x] and <class>.valid.ts[x]. --self-test proves rule fires on configured fixtures.
Reusable GitHub gate:
jobs:
slopgate:
uses: alexcodeplace/slopgate/.github/workflows/slopgate.yml@v1Self-hosted jobs reject fork pull requests before checkout. Use repository-owned branches or isolated disposable runners for untrusted forks.
Install or reinstall hooks:
slopgate install-hooks --config .slopgate/config.tomlCommand Reference
slopgate init [dir]
Onboard a new repository. Detects roots, extensions, installed checkers, and scaffolds project structure.
Args:
dir(optional) — target directory; defaults toprocess.cwd()- No
--configrequired; generates config during init
Creates:
.slopgate/config.toml— project config (roots, extensions, rule packs, checkers, baseline/suppressions paths).slopgate/suppressions.json— line-level violation suppressions (empty initially).slopgate/depcruise.cjs— starter dependency-cruiser rules (if depcruise detected).slopgate/convention-sources.json— hints for authoring project-specific rule packs.slopgate/rules/ast/and.slopgate/fixtures/src/— directories for custom rules and fixtures.git/hooks/pre-commit— native git pre-commit hook (creates new or appends to existing).claude/settings.json— Claude Code hook entries (idempotent merge)
Next step: Run slopgate baseline --config .slopgate/config.toml to create the initial ratchet baseline
slopgate --staged --config <path>
Run commit-tier gate on staged files. Used by git pre-commit hook and Claude Code PreToolUse hook.
Flags:
--config <path>(required) — path to.slopgate/config.toml--tier fast|commit(optional) — override default tier (default: commit for--staged)
Exit codes:
0— no violations (or all baselined/suppressed)1— violations block the commit2— config error or missing argument
Output:
- Violations grouped by source (regex, ast, checker:tsc, etc.)
- Baselined count footer
- Skipped checkers (if tool/config missing)
slopgate --file <path> --config <path>
Run fast-tier gate on a single file (post-edit). Used by Claude Code PostToolUse hook.
Flags:
--file <path>(required) — repo-relative path to check--config <path>(required) — path to.slopgate/config.toml--tier fast|commit(optional) — override default tier (default: fast for--file)
Exit codes: same as --staged
Output: violations in the touched file only; no baseline filtering
slopgate baseline --config <path> [--update] [--prune]
Manage the ratchet baseline.
Flags:
--config <path>(required)--update— re-snapshot all current violations (overwrites baseline)--prune— remove entries whose fingerprint no longer occurs (dry-run only)- Both flags can be combined;
--prune --updateprunes then updates
Behavior:
- No flags, file missing → create baseline with current violations
- No flags, file exists → error (refuses overwrite; use
--update) --update→ snapshot all violations in full commit tier scan--prune→ drop resolved fingerprints (non-destructive; just removes old entries)
slopgate install-hooks --config <path>
Install or upgrade the git pre-commit hook.
Flags:
--config <path>(required)
Behavior:
- No hook exists → create new hook with slopgate check
- Hook exists with slopgate marker → upgrade (idempotent)
- Foreign hook exists → append slopgate block before final
exec(preserves other hooks)
Hook location: <git-dir>/hooks/pre-commit (or respects git config core.hooksPath)
slopgate --self-test --config <path>
Internal: validate regex + AST engines and checker parsers against fixtures.
Flags:
--config <path>(required) — typicallyrules/baseline/selftest.config.toml
Runs in-process tests; exit 0 = all pass, exit 1 = failure. Used by npm run self-test.
How the Two Tiers Work
Fast Tier (Post-Edit)
Runs on every Edit/Write to a .ts, .tsx, or .astro file.
Scope: Single file Engines: Regex patterns + AST rules (baseline packs only) Baseline: Not consulted (all violations shown) Latency: < 1 second Feedback: Instant, in-editor
Rules applied:
- All regex patterns in enabled baseline packs (
no-stubs,ts-suppress,as-any, etc.) - All AST rules from enabled baseline packs
- Project-owned AST rules (from
astRulesconfig)
Commit Tier (Pre-Commit)
Runs before git commit or when --staged is called manually.
Scope: All staged files + full repo (for checkers like tsc, knip that need graph context) Engines: Regex patterns + AST rules + six heavy checkers Baseline: Consulted; only NEW violations block commit Latency: 5–30 seconds (tsc + knip dominate) Feedback: Commit blocked or passes
Rules applied:
- All regex patterns (same as fast tier)
- All AST rules (same as fast tier)
- tsc — TypeScript type errors (full-project compile)
- knip — unused exports/files/dependencies
- jscpd — copy-paste clones (staged files only are reported)
- dependency-cruiser — architecture violations
- type-coverage — NEW uncovered expressions
- diff-shape — staged files spanning > N top-level dirs
Filtering:
- Run all sources (regex, ast, checkers)
- Fingerprint violations (sha256 of source, rule, file, normalized message, line text)
- Filter by ratchet baseline (drop fingerprints in baseline.json)
- Filter by suppressions (line-level, per file + lineHash)
- Filter by severity gate (only show
critical/highby default, configurable) - Print report; exit 1 if violations remain
Ratchet Baseline
The ratchet prevents violations from blocking adoption of new rules or onboarding legacy repos.
How It Works
At init:
slopgate baseline --config ...creates.slopgate/baseline.jsonwith a snapshot of ALL current violations.On commit: The gate compares the current full-repo commit-tier scan against the baseline. Violations whose fingerprint is in the baseline are ignored (baselined); NEW violations block the commit.
Paydown: As issues are fixed, their fingerprint disappears from the current scan.
slopgate baseline --pruneremoves old entries from the baseline, lowering the bar.Re-snapshot:
slopgate baseline --updatedoes a full re-scan and updates the baseline (use after intentionally widening rules or adding new checkers).
Fingerprint Stability
Fingerprints include:
- Rule ID
- File path (repo-relative)
- Normalized message (digit runs replaced with
#, kills line/col churn) - First 60 chars of the source line (trimmed)
Fingerprints do NOT include the line number, so they survive unrelated edits shifting lines.
Suppressions vs. Baseline
- Baseline — temporary allowlist; debt should be paid down over time. Track in version control. Entire project-wide snapshot.
- Suppressions — permanent per-file exemptions (e.g., "this pattern is correct in this context"). Sparse, line-level. Also tracked.
UX Module (optional)
The UX module provides opinionated static analysis rules for common UX anti-patterns. It is off by default since UX preferences vary across teams and projects. Enable selectively via the ux:{} config namespace.
Why optional? Many teams have different UX preferences, and enabling UX rules on existing projects would flag pre-existing markup. These are good-enough defaults for NEW projects where you want opinionated UX guidance but have no specific opinion yourself.
Configuration
# .slopgate/config.toml
# ... other config
# UX module (optional) — off by default, opt-in per sub-module
[ux]
a11y = "high" # Accessibility violations (gate commits)
cls = "high" # Cumulative Layout Shift violations (gate commits)
feedback = "high" # Silent async / double-submit (gate commits)
taste = "advisory" # Design taste violations (report only, don't gate)
advisory = "advisory" # Heuristic nudges (report only, higher false-positive)
# taste = "medium" # equivalent to 'advisory'
# taste = true # use sub-module default severity
# omit key = that sub-module OFF
# delete whole [ux] table = entire module OFFSub-modules
| Key | Catches | Default Severity | Framework § |
|-----|---------|------------------|-------------|
| a11y | onClick on <div>/<span> without role; <a onClick> without href; <img> without alt; <button> without type; positive tabIndex | high | §11 |
| cls | <img>/<video>/<iframe> without width/height | high | §13 |
| feedback | async onClick on a <button> with no disabled state (double-submit, silent wait) | high | §3/§12 |
| taste | emoji in UI, "trusted by" clichés, Lorem ipsum, robotic microcopy, heavy drop shadows, linear/long (>300ms) motion | medium | §0/§6/§26 |
| advisory | modal without onClose; array index as React key; view state (tab/page/filter) in useState instead of the URL | medium | §10/§14 |
Magic hardcoded colors/spacing (#hex, rgb()/hsl(), multi-digit px) are caught by the baseline raw-hex pack (§15), independent of the UX module.
Severity Levels
'critical'/'high': Gates commits (blocks by default, since default gate is['critical','high'])'medium'/'advisory': Reports but doesn't block commits (useful for gradual adoption)true: Use the sub-module's default severity- Omit key: That sub-module is OFF
- Delete
ux:{}block: Entire UX module is OFF
Opt-out
Symmetric and trivial:
- Delete a key to disable one sub-module:
ux: { a11y: 'high' }(cls and taste OFF) - Delete the whole
ux:{}block to disable the entire module
Companion Skill
Pair the static UX module with the /slopgate-ux skill for semantic UX directives that static analysis can't enforce (four-states, button hierarchy, focus-trap, optimistic UI, etc.).
Config Reference (.slopgate/config.toml)
# Repository layout
roots = ["src"] # source roots to scan
exts = [".ts", ".tsx", ".astro"] # file extensions
skipDirs = ["node_modules", "dist"] # dirs to skip
# Rule packs
baseline = ["no-stubs", "ts-suppress", "as-any"] # built-in baseline packs to enable (opt-in)
rules = [] # project regex rule packs — must be [] (PHASE-2, not yet supported)
astRules = "./rules/ast" # dir of .yml AST rules (optional)
astDisable = [] # rule ids to disable (escape hatch)
# Custom file paths (relative to repo root)
suppressions = "./suppressions.json" # line-level exemptions
fixtures = "./fixtures" # test fixture canaries
# baselinePath is auto-computed: .slopgate/baseline.json
# Commit-tier checkers (detected at init; absent = off)
# Per-checker options as key = value under each [checkers.<name>] table.
[checkers.tsc]
# e.g. timeout = 60
# UX module (optional) — off by default, opt-in per sub-module
[ux]
a11y = "high" # accessibility violations
cls = "high" # cumulative layout shift
taste = "advisory" # design taste (reports, doesn't gate)
# Severity filtering (which violations show in reports)
[gate]
file = ["critical", "high"] # fast-tier report threshold
staged = ["critical", "high"] # commit-tier report thresholdAuto-generated during init:
roots— detected from workspace packages and src/ dirsexts— detected from file walkskipDirs— detected from common exclusions (node_modules, dist, tests, .worktrees)checkers— detected from installed binaries and config files (all true initially)
Rule Packs
Baseline Regex Packs (Shipped)
All are opt-in via the baseline array in config. Severity drives the gate threshold (critical/high block by default).
| Pack | Severity | Category | Catches |
|------|----------|----------|---------|
| no-stubs | critical | convention | Stub / placeholder / "not implemented" / deferred-work markers |
| ts-suppress | high | convention | @ts-ignore / @ts-expect-error — suppressing tsc instead of fixing the cause |
| as-any | high | convention | as any casts that disable type safety |
| no-narration-comments | high | convention | Comments that narrate a diff/history/removal (changed from, used to, no longer needed, track later) instead of explaining a non-obvious constraint |
| raw-hex | high | convention | Hardcoded hex / rgb() colors + raw multi-digit px — use design tokens |
| sql-safety | critical | convention | SELECT … FOR UPDATE with an aggregate (Postgres rejects this at runtime) |
| kv-ban | critical | boundary | Cloudflare KV in read-after-write paths (eventually-consistent) |
| live-secrets | critical | security | Hardcoded Stripe / webhook / Google live credentials |
| eval-ban | critical | security | eval / dynamic code execution (injection surface) |
| pii-logs | high | security | PII fields written to logs / error trackers |
| weak-hash | high | security | MD5 / SHA-1 for integrity checks or passwords (cryptographically broken) |
Baseline AST Rules (Shipped, Always Active)
Loaded automatically alongside the regex packs (the resolver always adds rules/baseline/ast); disable any by id via astDisable = [...].
| Rule id | Catches |
|---------|---------|
| empty-catch (ts + tsx) | Empty catch block silently swallowing an error |
| inner-html | Unsafe innerHTML / dangerouslySetInnerHTML assignment |
| target-blank-norel | target="_blank" anchor missing rel="noopener" |
| window-in-render | window/document access during render (SSR hazard) |
Stack Packs (Shipped)
Opt-in via stack = ["cloudflare"]:
| Pack | Rule ids |
|------|----------|
| cloudflare | cf-env-spread-secrets, process-env-access, waituntil-bare-method-ref, cf-getCloudflareContext-banned, hono-env-direct-access |
Planned (v2+):
- Depth rules — pass-through-fn, delegating-wrapper (Ousterhout symptoms)
- Test-slop rules — test-no-assertion, test-skip-only
- Custom project regex rule packs (the
rules = [...]field — see Project-Owned Rules)
Project-Owned Rules
Add custom rules as AST rules — .yml files in ast-grep syntax:
id: my-ast-rule
language: tsx
severity: error # ast-grep level (error|warning|info)
message: Rule violation
note: '{"severity":"high","category":"convention","resolution":"…"}' # slopgate metadata
rule:
pattern: 'someBadCall($$$ARGS)' # code-snippet matcher; or structural kind/has/inside/all/any/notPoint astRules at the directory holding them:
astRules = "./rules/ast" # auto-loads all .yml files in this dirNote: Custom project regex rule packs (the
rules = [...]field) are not yet supported by the native engine.rulesmust currently be[]; a non-empty value errors withslopgate: project rule pack "<path>" cannot be loaded by the native TOML resolver (PHASE-2: project rule packs). Project regex packs are planned (PHASE-2). For now, use ast-grep YAML for custom rules, or one of the built-in baseline/stack packs.
How Rules Are Authored
Regex Rules
Note: Authoring custom project regex rule packs is not yet supported by the native engine (PHASE-2 — see Project-Owned Rules). The shape below describes how the built-in regex packs are defined (compiled into the engine); it is reference, not a workflow you can wire in via
rulestoday. Use ast-grep YAML for custom rules.
Patterns are regex strings with flags (i, m, s, etc.). A pattern matches any line containing the regex.
Example:
{
id: 'no-stubs-placeholder',
pattern: 'placeholder\\s+(?:for now|impl)',
flags: 'i',
canary: '// placeholder for now',
negativeCanary: ['placeholder={t(\'x\')}'], // should NOT match
}Advanced:
minFiles: N— pattern must match in ≥ N files to fire (catch widespread slop)excludeGlobs: ['*.test.ts']— skip matching in these pathsincludeGlobs: ['src/**']— only match in these paths- Suppressions: per-file, per-line (lineHash = sha256 of line text)
AST Rules
Written in ast-grep YAML syntax; scoped to source roots + extensions from config.
Example (modeled on the shipped rules/baseline/ast/empty-catch-block-tsx.yml):
id: empty-catch
language: tsx
severity: error # ast-grep level (error|warning|info)
message: Empty catch block swallows error silently
note: '{"severity":"high","category":"convention","resolution":"Handle or rethrow; log with context."}'
rule:
pattern: 'try { $A } catch ($E) {}' # code-snippet matcher; or structural kind/has/inside/all/any/notThe top-level severity is ast-grep's own level; slopgate's gating severity/category/resolution live in the
JSON note field.
Fixtures: add a source canary that triggers the rule to .slopgate/fixtures/src/ (built-in rules use
rules/baseline/fixtures/src/). A .ts/.tsx file containing the violating code is enough:
// .slopgate/fixtures/src/empty-catch.tsx
export function f() { try { risky(); } catch (e) {} } // should fire empty-catchslopgate --self-test --config .slopgate/config.toml scans the fixtures and asserts every rule fires at
least once.
Hooks Integration
Claude Code Hooks
Init wires slopgate into .claude/settings.json:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/path/to/slopgate/hooks/commit-hook.sh"
}
]
}],
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "/path/to/slopgate/hooks/edit-hook.sh"
}
]
}]
}
}- PreToolUse (commit-hook.sh) — fires before Bash tool use; checks for
git commitin the command and runsslopgate --staged - PostToolUse (edit-hook.sh) — fires after Edit/Write; runs
slopgate --fileon the touched file (fast tier, 5-second timeout)
Git Pre-Commit Hook
init also installs .git/hooks/pre-commit (or appends to existing). This is the native git hook; it catches commits from any tool (terminal, IDE, other agents).
#!/usr/bin/env bash
ROOT=$(git rev-parse --show-toplevel) || exit 0
CONFIG="$ROOT/.slopgate/config.toml"
[ -f "$CONFIG" ] || exit 0
exec slopgate --staged --config "$CONFIG"The hook can be bypassed with git commit --no-verify, which is intentional (user-initiated escape hatch).
Suppressions
Edit .slopgate/suppressions.json:
{
"version": 1,
"entries": [
{
"ruleId": "no-stubs-placeholder",
"file": "src/app.ts",
"lineHash": "abc123def..."
}
]
}Line hash is auto-generated: sha256(trimmedLine).slice(0, 16).
To suppress a violation, grab the line hash from the report and add an entry. The line text must match exactly (trimmed); unrelated edits shift line numbers but keep line text stable.
Testing
Run Self-Test
npm run self-testValidates:
- Regex engine (patterns match canaries, skip negativeCanaries)
- AST engine (ast-grep rules parse + match fixtures)
- Checker parsers (tsc, knip, jscpd, depcruise, type-coverage outputs parse correctly)
- Ratchet fingerprints (stability under line shifts)
- Suppressions (line hashing, deduplication)
Examples
Example 1: Block Unsafe Type Casts
Config:
baseline = ["as-any"]
[gate]
staged = ["critical", "high"]Commit a file with const x = y as any;:
slopgate: 1 violation(s)
regex › as-any-cast
src/utils.ts:42
Unsafe `as any` cast
severity: high
resolution: Use a precise type or a discriminated narrowing.
exit code: 1 (commit blocked)Fix it to const x = y as unknown; or a proper type, then commit.
Example 2: Allow Pre-Existing Copy-Paste, Block New Ones
Config:
baseline = []
[checkers.jscpd]
minTokens = 50Run slopgate baseline --config .slopgate/config.toml to baseline existing clones. Now:
- Commits pass unless they introduce NEW duplications
- Track paydown via
slopgate baseline --prune(drops resolved entries)
Example 3: Custom Architecture Rules
Create .slopgate/depcruise.cjs:
module.exports = {
forbidden: [
{
name: 'no-ui-to-db',
severity: 'error',
from: { path: 'src/ui' },
to: { path: 'src/db' },
},
],
};Now commits that import database code from UI layer are blocked.
Example 4: Silence a Built-in Rule in One Project
Config:
baseline = ["no-stubs", "as-any"]
astDisable = ["target-blank-norel"] # this app links only to vetted internal routesastDisable lists built-in AST rule ids to turn off for this repo; every other rule stays active.
Architecture
Data Flow (Commit Tier)
git commit
└─ .git/hooks/pre-commit
└─ slopgate --staged --config <repo>/.slopgate/config.toml
├─ Enumerate staged files
├─ Regex engine (patterns → violations)
├─ AST engine (ast-grep rules → violations)
├─ Checker adapters
│ ├─ tsc (type errors)
│ ├─ knip (dead code)
│ ├─ jscpd (duplication)
│ ├─ dependency-cruiser (architecture)
│ ├─ type-coverage (any propagation)
│ └─ diff-shape (mixed concerns)
├─ Ratchet baseline filter (drop pre-existing)
├─ Suppressions filter (per-file, per-line)
├─ Severity gate (critical/high)
└─ Report + exit code (0 = pass, 1 = blocked)Checker Timeout and Errors
Each checker has a per-tool timeout (configurable):
- tsc: 120s
- knip: 90s
- jscpd: 60s
- depcruise: 60s
- type-coverage: 120s
Tool crash / timeout → ⚠ skipped: <id> (<reason>) warning, gate continues (fail-open on infra). Violations still block; missing tools don't.
Limitations & Future Work
- Git-only — no other VCS support
- No auto-fix — violations are reported, not automatically corrected
slopgate auditcommand — planned for v2 (non-gating architecture-health report: hotspots, module shape, co-change coupling, ratchet progress tracking)- Embeddings-based semantic duplicate detection — planned, not in v1
- API-surface diff gate — track breaking changes to public exports (future)
- LLM-judge skill — on-demand deep review of architectural debt (separate sub-project)
Contributing
See CONTRIBUTING.md.
License
MIT — See LICENSE for details.
