@alexcodeplace/slopgate 0.3.1 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -11
- package/bin/slopgate +49 -3
- package/hooks/baseline-guard.mjs +88 -0
- package/hooks/baseline-guard.sh +13 -58
- package/hooks/commit-hook.sh +8 -3
- package/hooks/edit-hook.sh +5 -3
- package/hooks/runtime.sh +28 -0
- package/hooks/session-start.sh +3 -1
- package/hooks/test-hooks-decisions.sh +102 -0
- package/package.json +4 -1
- package/rules/baseline/ast/focused-test-only-ts.yml +10 -0
- package/rules/baseline/ast/focused-test-only-tsx.yml +10 -0
- package/rules/baseline/ast/focused-test-shorthand-ts.yml +15 -0
- package/rules/baseline/ast/focused-test-shorthand-tsx.yml +15 -0
- package/rules/baseline/ast/inner-html.yml +4 -1
- package/rules/baseline/fixtures/checker-outputs/actionlint.expected.json +9 -0
- package/rules/baseline/fixtures/checker-outputs/actionlint.json +11 -0
- package/rules/baseline/fixtures/checker-outputs/shellcheck.expected.json +16 -0
- package/rules/baseline/fixtures/checker-outputs/shellcheck.json +18 -0
- package/rules/baseline/fixtures/checker-outputs/typos.expected.json +14 -0
- package/rules/baseline/fixtures/checker-outputs/typos.jsonl +3 -0
- package/rules/baseline/fixtures/src/canary.tsx +6 -0
- package/rules/baseline/fixtures/src/focused-test.ts +11 -0
- package/vendor/darwin-arm64/slopgate-rs +0 -0
- package/vendor/darwin-x64/slopgate-rs +0 -0
- package/vendor/linux-arm64/slopgate-rs +0 -0
- package/vendor/linux-x64/slopgate-rs +0 -0
- package/vendor/win32-x64/slopgate-rs.exe +0 -0
package/README.md
CHANGED
|
@@ -41,10 +41,10 @@ A global code-quality / anti-slop gate for Claude Code and git. Engine is shared
|
|
|
41
41
|
## Install
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
npm install -g slopgate
|
|
44
|
+
npm install -g @alexcodeplace/slopgate@0.3.4
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
The matching prebuilt native engine for your platform
|
|
47
|
+
The matching prebuilt native engine for your platform is included in the package, and the pinned `ast-grep` runtime needed for structural rules is installed as a package dependency. No Rust toolchain or separate scanner install is required.
|
|
48
48
|
|
|
49
49
|
Then onboard a project:
|
|
50
50
|
|
|
@@ -90,9 +90,31 @@ slopgate baseline --prune --config .slopgate/config.toml
|
|
|
90
90
|
|
|
91
91
|
### Run self-test (validate the engine against bundled fixtures):
|
|
92
92
|
```bash
|
|
93
|
-
slopgate --self-test --config "$(npm root -g)/slopgate/rules/baseline/selftest.config.toml"
|
|
93
|
+
slopgate --self-test --config "$(npm root -g)/@alexcodeplace/slopgate/rules/baseline/selftest.config.toml"
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
### Run immutable full-repository CI gate:
|
|
97
|
+
```bash
|
|
98
|
+
slopgate scan --scope repo --tier commit --format github --config .slopgate/config.toml
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Record reviewer defects and enforce rule harvesting:
|
|
102
|
+
```bash
|
|
103
|
+
slopgate defect record --class missing-button-type --file src/app.tsx --line 42 --source code-review --config .slopgate/config.toml
|
|
104
|
+
slopgate harvest --check --config .slopgate/config.toml
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Second 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.
|
|
108
|
+
|
|
109
|
+
### Reusable GitHub gate:
|
|
110
|
+
```yaml
|
|
111
|
+
jobs:
|
|
112
|
+
slopgate:
|
|
113
|
+
uses: alexcodeplace/slopgate/.github/workflows/slopgate.yml@v1
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The reusable workflow defaults to GitHub-hosted `ubuntu-latest`, so fork pull requests run in an isolated disposable runner. If you override `runner-json` to use a persistent self-hosted runner, do not execute untrusted fork code there.
|
|
117
|
+
|
|
96
118
|
### Install or reinstall hooks:
|
|
97
119
|
```bash
|
|
98
120
|
slopgate install-hooks --config .slopgate/config.toml
|
|
@@ -343,7 +365,7 @@ skipDirs = ["node_modules", "dist"] # dirs to skip
|
|
|
343
365
|
|
|
344
366
|
# Rule packs
|
|
345
367
|
baseline = ["no-stubs", "ts-suppress", "as-any"] # built-in baseline packs to enable (opt-in)
|
|
346
|
-
rules = []
|
|
368
|
+
rules = ["./rules/project.json"] # optional project-owned regex packs
|
|
347
369
|
astRules = "./rules/ast" # dir of .yml AST rules (optional)
|
|
348
370
|
astDisable = [] # rule ids to disable (escape hatch)
|
|
349
371
|
|
|
@@ -387,7 +409,7 @@ All are opt-in via the `baseline` array in config. Severity drives the gate thre
|
|
|
387
409
|
|------|----------|----------|---------|
|
|
388
410
|
| `no-stubs` | critical | convention | Stub / placeholder / "not implemented" / deferred-work markers |
|
|
389
411
|
| `ts-suppress` | high | convention | `@ts-ignore` / `@ts-expect-error` — suppressing tsc instead of fixing the cause |
|
|
390
|
-
| `as-any` | high | convention | `as any`
|
|
412
|
+
| `as-any` | high | convention | `as any`, `: any`, `Array<any>`, `Promise<any>`, `Record<string, any>` escape hatches that disable type safety |
|
|
391
413
|
| `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 |
|
|
392
414
|
| `raw-hex` | high | convention | Hardcoded hex / `rgb()` colors + raw multi-digit `px` — use design tokens |
|
|
393
415
|
| `sql-safety` | critical | convention | `SELECT … FOR UPDATE` with an aggregate (Postgres rejects this at runtime) |
|
|
@@ -405,6 +427,7 @@ Loaded automatically alongside the regex packs (the resolver always adds `rules/
|
|
|
405
427
|
|---------|---------|
|
|
406
428
|
| `empty-catch` (ts + tsx) | Empty `catch` block silently swallowing an error |
|
|
407
429
|
| `inner-html` | Unsafe `innerHTML` / `dangerouslySetInnerHTML` assignment |
|
|
430
|
+
| `focused-test` | Focused tests committed via `test.only` / `it.only` / `describe.only` / `fit` / `fdescribe` |
|
|
408
431
|
| `target-blank-norel` | `target="_blank"` anchor missing `rel="noopener"` |
|
|
409
432
|
| `window-in-render` | `window`/`document` access during render (SSR hazard) |
|
|
410
433
|
|
|
@@ -418,7 +441,7 @@ Opt-in via `stack = ["cloudflare"]`:
|
|
|
418
441
|
|
|
419
442
|
**Planned (v2+):**
|
|
420
443
|
- Depth rules — pass-through-fn, delegating-wrapper (Ousterhout symptoms)
|
|
421
|
-
- Test-slop rules — test-no-assertion, test-skip
|
|
444
|
+
- Test-slop rules — test-no-assertion, test-skip
|
|
422
445
|
- Custom project **regex** rule packs (the `rules = [...]` field — see [Project-Owned Rules](#project-owned-rules))
|
|
423
446
|
|
|
424
447
|
### Project-Owned Rules
|
|
@@ -441,7 +464,7 @@ Point `astRules` at the directory holding them:
|
|
|
441
464
|
astRules = "./rules/ast" # auto-loads all .yml files in this dir
|
|
442
465
|
```
|
|
443
466
|
|
|
444
|
-
|
|
467
|
+
Project-owned regex rule packs are supported as JSON files listed in `rules = ["./rules/my-pack.json"]`. Use regex rules for precise line/token patterns and ast-grep YAML for structural code shapes.
|
|
445
468
|
|
|
446
469
|
---
|
|
447
470
|
|
|
@@ -449,7 +472,7 @@ astRules = "./rules/ast" # auto-loads all .yml files in this dir
|
|
|
449
472
|
|
|
450
473
|
### Regex Rules
|
|
451
474
|
|
|
452
|
-
|
|
475
|
+
Custom project regex packs use the same keyed JSON shape as the built-in packs and are loaded from paths listed in `rules = [...]` in `.slopgate/config.toml`.
|
|
453
476
|
|
|
454
477
|
Patterns are regex strings with flags (i, m, s, etc.). A pattern matches any line containing the regex.
|
|
455
478
|
|
|
@@ -701,12 +724,10 @@ Tool crash / timeout → `⚠ skipped: <id> (<reason>)` warning, gate continues
|
|
|
701
724
|
|
|
702
725
|
- **Git-only** — no other VCS support
|
|
703
726
|
- **No auto-fix** — violations are reported, not automatically corrected
|
|
704
|
-
-
|
|
705
|
-
- **`slopgate audit` command** — planned for v2 (non-gating architecture-health report: hotspots, module shape, co-change coupling, ratchet progress tracking)
|
|
727
|
+
- **`slopgate audit` is advisory** — it reports architecture/ratchet health but does not gate commits
|
|
706
728
|
- **Embeddings-based semantic duplicate detection** — planned, not in v1
|
|
707
729
|
- **API-surface diff gate** — track breaking changes to public exports (future)
|
|
708
730
|
- **LLM-judge skill** — on-demand deep review of architectural debt (separate sub-project)
|
|
709
|
-
- **Rule harvesting** — auto-generate rules from repeated violations (separate sub-project)
|
|
710
731
|
|
|
711
732
|
---
|
|
712
733
|
|
package/bin/slopgate
CHANGED
|
@@ -11,21 +11,67 @@
|
|
|
11
11
|
// 2. <pkg>/vendor/<platform>-<arch>/slopgate-rs (bundled prebuilt; npm-install path)
|
|
12
12
|
// 3. <pkg>/target/release/slopgate-rs (cargo build --release; dev)
|
|
13
13
|
import { spawnSync } from 'node:child_process';
|
|
14
|
-
import { existsSync } from 'node:fs';
|
|
14
|
+
import { existsSync, realpathSync, readFileSync } from 'node:fs';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import { dirname, join } from 'node:path';
|
|
17
|
+
import os from 'node:os';
|
|
18
|
+
|
|
19
|
+
// Re-entry guard: if a resolution bug ever routes the engine back to this shim,
|
|
20
|
+
// fail closed instead of forking forever (a self-pointing $SLOPGATE_BIN froze a
|
|
21
|
+
// workstation with an unbounded nice -10 spawn chain).
|
|
22
|
+
if (process.env.SLOPGATE_SHIM_ACTIVE === '1') {
|
|
23
|
+
process.stderr.write(
|
|
24
|
+
'slopgate: recursive shim invocation detected (SLOPGATE_BIN points back at the slopgate launcher?) — aborting.\n',
|
|
25
|
+
);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
process.env.SLOPGATE_SHIM_ACTIVE = '1';
|
|
29
|
+
|
|
30
|
+
// Git hooks inherit the caller's nice; never let the gate outrank the desktop.
|
|
31
|
+
try {
|
|
32
|
+
if (os.getPriority() < 0) os.setPriority(0);
|
|
33
|
+
} catch {}
|
|
34
|
+
|
|
35
|
+
const selfPath = fileURLToPath(import.meta.url);
|
|
36
|
+
const pkgRoot = join(dirname(selfPath), '..');
|
|
37
|
+
|
|
38
|
+
// Report the public package version, not the Rust engine's internal crate version.
|
|
39
|
+
if (process.argv.length === 3 && ['--version', '-V'].includes(process.argv[2])) {
|
|
40
|
+
try {
|
|
41
|
+
const { version } = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8'));
|
|
42
|
+
process.stdout.write(`slopgate ${version}\n`);
|
|
43
|
+
process.exit(0);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
process.stderr.write(`slopgate: failed to read package version: ${error.message}\n`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Runtime dependencies such as ast-grep live inside this npm package. Make
|
|
51
|
+
// their bins visible to the native engine even when slopgate is installed
|
|
52
|
+
// globally and the target project has no node_modules of its own.
|
|
53
|
+
const depBin = join(pkgRoot, 'node_modules', '.bin');
|
|
54
|
+
const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === 'path') || 'PATH';
|
|
55
|
+
process.env[pathKey] = process.env[pathKey] ? `${depBin}${process.platform === 'win32' ? ';' : ':'}${process.env[pathKey]}` : depBin;
|
|
17
56
|
|
|
18
|
-
const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
57
|
const binName = process.platform === 'win32' ? 'slopgate-rs.exe' : 'slopgate-rs';
|
|
20
58
|
const host = `${process.platform}-${process.arch}`;
|
|
21
59
|
|
|
60
|
+
const isSelf = (p) => {
|
|
61
|
+
try {
|
|
62
|
+
return realpathSync(p) === realpathSync(selfPath);
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
22
68
|
const candidates = [
|
|
23
69
|
process.env.SLOPGATE_BIN,
|
|
24
70
|
join(pkgRoot, 'vendor', host, binName),
|
|
25
71
|
join(pkgRoot, 'target', 'release', binName),
|
|
26
72
|
].filter(Boolean);
|
|
27
73
|
|
|
28
|
-
const bin = candidates.find((p) => existsSync(p));
|
|
74
|
+
const bin = candidates.find((p) => existsSync(p) && !isSelf(p));
|
|
29
75
|
if (!bin) {
|
|
30
76
|
process.stderr.write(
|
|
31
77
|
`slopgate: native engine binary not found for ${host}.\n` +
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Slopgate baseline-guard — PreToolUse hook blocking agent bypass of the gate.
|
|
2
|
+
// Covers: Edit/Write to baseline.json/suppressions.json, Bash rm/mv on baseline*,
|
|
3
|
+
// Bash `slopgate baseline` in any form.
|
|
4
|
+
// Exit 2 = block the tool use (PreToolUse protocol). Exit 0 = allow. Always fail-open on errors.
|
|
5
|
+
|
|
6
|
+
import crypto from "node:crypto";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
|
|
12
|
+
const BASELINE_FILE = /\.slopgate\/(baseline|suppressions)\.json$/;
|
|
13
|
+
const BASELINE_CMD =
|
|
14
|
+
/(^|[;&|\s])(npx\s+|pnpm\s+exec\s+|yarn\s+)?(node\s+[^ ]+\/bin\/slopgate|slopgate)\s+baseline(\s|$)/;
|
|
15
|
+
const BASELINE_RM = /(rm|mv)\s+.*\.slopgate\/baseline/;
|
|
16
|
+
|
|
17
|
+
function payload() {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(fs.readFileSync(0, "utf8"));
|
|
20
|
+
} catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function verdict(input) {
|
|
26
|
+
const toolInput = input.tool_input || {};
|
|
27
|
+
switch (input.tool_name) {
|
|
28
|
+
case "Edit":
|
|
29
|
+
case "Write": {
|
|
30
|
+
const filePath = String(toolInput.file_path || "");
|
|
31
|
+
if (BASELINE_FILE.test(filePath)) {
|
|
32
|
+
return `direct write to ${filePath} blocked — edit baseline/suppressions via slopgate CLI only`;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
case "Bash": {
|
|
37
|
+
const command = String(toolInput.command || "");
|
|
38
|
+
if (BASELINE_RM.test(command)) {
|
|
39
|
+
return "rm/mv of .slopgate/baseline blocked — baseline integrity guard";
|
|
40
|
+
}
|
|
41
|
+
if (BASELINE_CMD.test(command)) {
|
|
42
|
+
return "slopgate baseline blocked — agent cannot update/create baseline; run in your own terminal";
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
default:
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function recordBypassAttempt(reason) {
|
|
52
|
+
let root;
|
|
53
|
+
try {
|
|
54
|
+
root = execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
55
|
+
} catch {
|
|
56
|
+
root = process.cwd();
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const key = crypto.createHash("sha256").update(root).digest("hex").slice(0, 16);
|
|
60
|
+
const sess = JSON.parse(fs.readFileSync(path.join(os.homedir(), ".slopgate", "sessions", `${key}.json`), "utf8"));
|
|
61
|
+
const row = JSON.stringify({
|
|
62
|
+
ts: new Date().toISOString(),
|
|
63
|
+
project: path.basename(root),
|
|
64
|
+
projectPath: root,
|
|
65
|
+
model: sess.model || "unknown",
|
|
66
|
+
sessionId: sess.sessionId || null,
|
|
67
|
+
mode: "bypass-attempt",
|
|
68
|
+
ruleId: "baseline-tamper",
|
|
69
|
+
severity: "critical",
|
|
70
|
+
category: "security",
|
|
71
|
+
engine: "bypass-attempt",
|
|
72
|
+
file: null,
|
|
73
|
+
line: null,
|
|
74
|
+
reason
|
|
75
|
+
});
|
|
76
|
+
fs.appendFileSync(path.join(os.homedir(), ".slopgate", "stats.jsonl"), `${row}\n`);
|
|
77
|
+
} catch {
|
|
78
|
+
// fail-open: the deny still stands even when stats cannot be written
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const reason = verdict(payload());
|
|
83
|
+
if (reason) {
|
|
84
|
+
fs.writeSync(2, `⛔ SLOPGATE GUARD: ${reason}\n`);
|
|
85
|
+
recordBypassAttempt(reason);
|
|
86
|
+
process.exit(2);
|
|
87
|
+
}
|
|
88
|
+
process.exit(0);
|
package/hooks/baseline-guard.sh
CHANGED
|
@@ -1,60 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
tool_input=$(node -e "try{const j=JSON.parse(process.argv[1]);process.stdout.write(JSON.stringify(j.tool_input||{}))}catch{}" "$INPUT" 2>/dev/null || echo '{}')
|
|
12
|
-
|
|
13
|
-
blocked=0
|
|
14
|
-
reason=''
|
|
15
|
-
|
|
16
|
-
case "$tool_name" in
|
|
17
|
-
Edit|Write)
|
|
18
|
-
file_path=$(node -e "try{const j=JSON.parse(process.argv[1]);process.stdout.write(j.file_path||'')}catch{}" "$tool_input" 2>/dev/null || echo '')
|
|
19
|
-
if echo "$file_path" | grep -qE '\.slopgate/(baseline|suppressions)\.json$'; then
|
|
20
|
-
blocked=1
|
|
21
|
-
reason="direct write to $file_path blocked — edit baseline/suppressions via slopgate CLI only"
|
|
22
|
-
fi
|
|
23
|
-
;;
|
|
24
|
-
Bash)
|
|
25
|
-
cmd=$(node -e "try{const j=JSON.parse(process.argv[1]);process.stdout.write(j.command||'')}catch{}" "$tool_input" 2>/dev/null || echo '')
|
|
26
|
-
# Block: slopgate baseline (create or --update)
|
|
27
|
-
if echo "$cmd" | grep -qE '(^|[;&|[:space:]])(npx[[:space:]]+|pnpm[[:space:]]+exec[[:space:]]+|yarn[[:space:]]+)?(node[[:space:]]+[^ ]+/bin/slopgate|slopgate)[[:space:]]+baseline([[:space:]]|$)'; then
|
|
28
|
-
blocked=1
|
|
29
|
-
reason="slopgate baseline blocked — agent cannot update/create baseline; run in your own terminal"
|
|
30
|
-
fi
|
|
31
|
-
# Block: rm/mv targeting .slopgate/baseline*
|
|
32
|
-
if echo "$cmd" | grep -qE '(rm|mv)\s+.*\.slopgate/baseline'; then
|
|
33
|
-
blocked=1
|
|
34
|
-
reason="rm/mv of .slopgate/baseline blocked — baseline integrity guard"
|
|
35
|
-
fi
|
|
36
|
-
;;
|
|
2
|
+
# Fast-path launcher: every trigger pattern in baseline-guard.mjs requires the literal
|
|
3
|
+
# "slopgate" somewhere in the payload (.slopgate/ paths, `slopgate baseline` commands).
|
|
4
|
+
# The common call carries no such substring, so a pure-bash check answers it without a
|
|
5
|
+
# runtime start (measured: ~67ms runtime boot -> ~5ms). Payload with the literal falls
|
|
6
|
+
# through to the real guard, verdict semantics unchanged.
|
|
7
|
+
payload=""; IFS= read -rd '' payload || true
|
|
8
|
+
case "$payload" in
|
|
9
|
+
*slopgate*) ;;
|
|
10
|
+
*) exit 0 ;;
|
|
37
11
|
esac
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
|
43
|
-
node -e "
|
|
44
|
-
const crypto=require('crypto'),fs=require('fs'),path=require('path'),os=require('os');
|
|
45
|
-
const root=process.argv[1], reason=process.argv[2];
|
|
46
|
-
try {
|
|
47
|
-
const key=crypto.createHash('sha256').update(root).digest('hex').slice(0,16);
|
|
48
|
-
const sess=JSON.parse(fs.readFileSync(path.join(os.homedir(),'.slopgate','sessions',key+'.json'),'utf8'));
|
|
49
|
-
const row=JSON.stringify({ts:new Date().toISOString(),project:path.basename(root),projectPath:root,
|
|
50
|
-
model:sess.model||'unknown',sessionId:sess.sessionId||null,mode:'bypass-attempt',
|
|
51
|
-
ruleId:'baseline-tamper',severity:'critical',category:'security',engine:'bypass-attempt',
|
|
52
|
-
file:null,line:null,reason});
|
|
53
|
-
const gp=path.join(os.homedir(),'.slopgate','stats.jsonl');
|
|
54
|
-
fs.appendFileSync(gp,row+'\n');
|
|
55
|
-
} catch { /* fail-open */ }
|
|
56
|
-
" "$ROOT" "$reason" 2>/dev/null || true
|
|
57
|
-
exit 2
|
|
58
|
-
fi
|
|
59
|
-
|
|
60
|
-
exit 0
|
|
12
|
+
HERE="${BASH_SOURCE[0]%/*}"
|
|
13
|
+
. "$HERE/runtime.sh"
|
|
14
|
+
RUNTIME="$(slopgate_runtime_or_warn baseline-guard)" || exit 0
|
|
15
|
+
printf '%s' "$payload" | "$RUNTIME" "$HERE/baseline-guard.mjs"
|
package/hooks/commit-hook.sh
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Slopgate PreToolUse hook — runs --staged before a git commit. Exit 1 → commit blocked.
|
|
3
3
|
TOOL_JSON=$(cat)
|
|
4
|
-
|
|
4
|
+
# Cheap raw prefilter: a git commit always carries the substring "commit" in the payload,
|
|
5
|
+
# so skip the interpreter spawn on every non-commit Bash call.
|
|
6
|
+
printf '%s' "$TOOL_JSON" | grep -q 'commit' || exit 0
|
|
7
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
. "$HERE/runtime.sh"
|
|
9
|
+
RUNTIME="$(slopgate_runtime_or_warn commit-hook)" || exit 0
|
|
10
|
+
CMD=$("$RUNTIME" -e "
|
|
5
11
|
let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{process.stdout.write(JSON.parse(d).tool_input?.command||'')}catch{process.stdout.write('')}});" <<< "$TOOL_JSON" 2>/dev/null)
|
|
6
12
|
echo "$CMD" | grep -qE 'git[[:space:]]+commit' || exit 0
|
|
7
13
|
|
|
8
14
|
ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
|
|
9
15
|
CONFIG="$ROOT/.slopgate/config.toml"
|
|
10
16
|
[ -f "$CONFIG" ] || exit 0
|
|
11
|
-
|
|
12
|
-
exec node "$HERE/../bin/slopgate" --staged --config "$CONFIG"
|
|
17
|
+
exec "$RUNTIME" "$HERE/../bin/slopgate" --staged --config "$CONFIG"
|
package/hooks/edit-hook.sh
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Slopgate PostToolUse hook — single-file scan after Edit/Write.
|
|
3
3
|
# Exit 2 → stderr feeds back into the agent turn. FAIL-OPEN: any error/timeout → exit 0.
|
|
4
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
. "$HERE/runtime.sh"
|
|
6
|
+
RUNTIME="$(slopgate_runtime_or_warn edit-hook)" || exit 0
|
|
4
7
|
TOOL_JSON=$(cat)
|
|
5
|
-
FILE=$(
|
|
8
|
+
FILE=$("$RUNTIME" -e "
|
|
6
9
|
let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{process.stdout.write(JSON.parse(d).tool_input?.file_path||'')}catch{process.stdout.write('')}});" <<< "$TOOL_JSON" 2>/dev/null) || exit 0
|
|
7
10
|
[ -n "$FILE" ] || exit 0
|
|
8
11
|
case "$FILE" in *.test.ts|*.test.tsx) exit 0 ;; *.ts|*.tsx|*.astro) ;; *) exit 0 ;; esac
|
|
@@ -13,7 +16,6 @@ ROOT=$(git -C "$(dirname "$FILE")" rev-parse --show-toplevel 2>/dev/null) || exi
|
|
|
13
16
|
CONFIG="$ROOT/.slopgate/config.toml"
|
|
14
17
|
[ -f "$CONFIG" ] || exit 0
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
OUT=$(timeout 5 node "$HERE/../bin/slopgate" --file "$FILE" --config "$CONFIG" 2>&1)
|
|
19
|
+
OUT=$(timeout 5 "$RUNTIME" "$HERE/../bin/slopgate" --file "$FILE" --config "$CONFIG" 2>&1)
|
|
18
20
|
[ "$?" -eq 1 ] && { echo "$OUT" >&2; exit 2; }
|
|
19
21
|
exit 0
|
package/hooks/runtime.sh
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Resolves the JS runtime the hooks exec. /usr/bin/bun is the workstation's real binary and
|
|
3
|
+
# is tried first so the hot hooks skip the PATH bun wrapper; a host without it (buildboxes)
|
|
4
|
+
# reaches bun or node through PATH, and every hook payload runs on either. A candidate must
|
|
5
|
+
# be named bun or node: a runtime pointed back at a hook would re-enter it on every exec.
|
|
6
|
+
# SLOPGATE_RUNTIME pins one explicitly; SLOPGATE_SYSTEM_RUNTIME overrides the absolute
|
|
7
|
+
# fast-path candidate (set empty to force PATH resolution).
|
|
8
|
+
slopgate_runtime() { # -> path to bun or node on stdout, 1 when neither resolves
|
|
9
|
+
local c
|
|
10
|
+
for c in "${SLOPGATE_RUNTIME:-}" "${SLOPGATE_SYSTEM_RUNTIME-/usr/bin/bun}"; do
|
|
11
|
+
case "${c##*/}" in bun | node) ;; *) continue ;; esac
|
|
12
|
+
[ -x "$c" ] || continue
|
|
13
|
+
printf '%s\n' "$c"
|
|
14
|
+
return 0
|
|
15
|
+
done
|
|
16
|
+
c="$(command -v bun || command -v node)" || return 1
|
|
17
|
+
[ -n "$c" ] || return 1
|
|
18
|
+
printf '%s\n' "$c"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
slopgate_runtime_or_warn() { # hook-name -> runtime on stdout; warns once, never blocks
|
|
22
|
+
local rt
|
|
23
|
+
rt="$(slopgate_runtime)" || {
|
|
24
|
+
echo "slopgate $1: no bun or node on this host — hook did nothing" >&2
|
|
25
|
+
return 1
|
|
26
|
+
}
|
|
27
|
+
printf '%s\n' "$rt"
|
|
28
|
+
}
|
package/hooks/session-start.sh
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
# Slopgate SessionStart hook — capture the session-start model for stats attribution.
|
|
3
3
|
# Only SessionStart receives a `model` field; mid-session /model switches are invisible.
|
|
4
4
|
# Fail-open: any error leaves no file -> stats resolves model to 'unknown'.
|
|
5
|
+
. "${BASH_SOURCE[0]%/*}/runtime.sh"
|
|
6
|
+
RUNTIME="$(slopgate_runtime_or_warn session-start)" || exit 0
|
|
5
7
|
ROOT=$(realpath "$(git rev-parse --show-toplevel 2>/dev/null || pwd)" 2>/dev/null || git rev-parse --show-toplevel 2>/dev/null || pwd)
|
|
6
|
-
exec
|
|
8
|
+
exec "$RUNTIME" -e '
|
|
7
9
|
const crypto = require("crypto"), fs = require("fs"), path = require("path"), os = require("os");
|
|
8
10
|
const root = process.argv[1];
|
|
9
11
|
let d = "";
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Decision regression tests for the agent-facing hooks (baseline-guard, commit-hook,
|
|
3
|
+
# session-start). Guards the block/allow contract across interpreter changes.
|
|
4
|
+
# HOME is sandboxed so stats/session writes never touch the real ~/.slopgate.
|
|
5
|
+
# Run: bash hooks/test-hooks-decisions.sh (exit 0 = all pass)
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
SANDBOX="$(mktemp -d)"
|
|
9
|
+
trap 'rm -rf "$SANDBOX"' EXIT
|
|
10
|
+
export HOME="$SANDBOX"
|
|
11
|
+
|
|
12
|
+
pass=0; fail=0
|
|
13
|
+
chk(){ # script expected_rc label payload
|
|
14
|
+
printf '%s' "$4" | bash "$HERE/$1" >/dev/null 2>&1
|
|
15
|
+
local rc=$?
|
|
16
|
+
if [ "$rc" -eq "$2" ]; then echo "PASS ($rc) $3"; pass=$((pass+1))
|
|
17
|
+
else echo "FAIL (got $rc want $2) $3"; fail=$((fail+1)); fi
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
echo "== baseline-guard (2=block, 0=allow) =="
|
|
21
|
+
chk baseline-guard.sh 2 "Edit baseline.json blocked" '{"tool_name":"Edit","tool_input":{"file_path":"/x/.slopgate/baseline.json"}}'
|
|
22
|
+
chk baseline-guard.sh 2 "Edit suppressions.json blocked" '{"tool_name":"Edit","tool_input":{"file_path":"/x/.slopgate/suppressions.json"}}'
|
|
23
|
+
chk baseline-guard.sh 0 "Edit normal file allowed" '{"tool_name":"Edit","tool_input":{"file_path":"/x/src/app.ts"}}'
|
|
24
|
+
chk baseline-guard.sh 2 "Bash slopgate baseline blocked" '{"tool_name":"Bash","tool_input":{"command":"slopgate baseline --update"}}'
|
|
25
|
+
BJSON=".slopgate/baseline.json"
|
|
26
|
+
chk baseline-guard.sh 2 "Bash rm baseline blocked" "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"rm $BJSON\"}}"
|
|
27
|
+
chk baseline-guard.sh 0 "Bash benign allowed" '{"tool_name":"Bash","tool_input":{"command":"ls -la"}}'
|
|
28
|
+
BCMD="slopgate ""baseline"
|
|
29
|
+
chk baseline-guard.sh 2 "Write baseline.json blocked" '{"tool_name":"Write","tool_input":{"file_path":"/x/.slopgate/baseline.json"}}'
|
|
30
|
+
chk baseline-guard.sh 2 "Bash npx wrapper blocked" "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"npx $BCMD\"}}"
|
|
31
|
+
chk baseline-guard.sh 2 "Bash node bin wrapper blocked" "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"node ./dist/bin/$BCMD --update\"}}"
|
|
32
|
+
chk baseline-guard.sh 2 "Bash mv baseline blocked" "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"mv $BJSON /tmp/x\"}}"
|
|
33
|
+
chk baseline-guard.sh 0 "Read is not gated" '{"tool_name":"Read","tool_input":{"file_path":"/x/.slopgate/baseline.json"}}'
|
|
34
|
+
chk baseline-guard.sh 0 "empty payload allowed" ''
|
|
35
|
+
|
|
36
|
+
# A block must still emit the operator-facing reason and the bypass-attempt stats row.
|
|
37
|
+
mkdir -p "$SANDBOX/.slopgate/sessions"
|
|
38
|
+
skey=$(printf '%s' "$PWD" | sha256sum | cut -c1-16)
|
|
39
|
+
printf '{"model":"opus","sessionId":"s1"}' > "$SANDBOX/.slopgate/sessions/$skey.json"
|
|
40
|
+
blk_err=$(printf '%s' '{"tool_name":"Edit","tool_input":{"file_path":"/x/.slopgate/baseline.json"}}' | bash "$HERE/baseline-guard.sh" 2>&1 >/dev/null)
|
|
41
|
+
if [[ "$blk_err" == *"SLOPGATE GUARD"* ]]; then echo "PASS (0) block prints the guard reason"; pass=$((pass+1));
|
|
42
|
+
else echo "FAIL block prints the guard reason: $blk_err"; fail=$((fail+1)); fi
|
|
43
|
+
if grep -q '"ruleId":"baseline-tamper"' "$SANDBOX/.slopgate/stats.jsonl" 2>/dev/null; then
|
|
44
|
+
echo "PASS (0) block records a bypass-attempt stats row"; pass=$((pass+1));
|
|
45
|
+
else echo "FAIL block records a bypass-attempt stats row"; fail=$((fail+1)); fi
|
|
46
|
+
|
|
47
|
+
echo "== commit-hook (0=allow/skip; no .slopgate config in sandbox HOME) =="
|
|
48
|
+
chk commit-hook.sh 0 "non-commit Bash fast-skips" '{"tool_name":"Bash","tool_input":{"command":"ls -la"}}'
|
|
49
|
+
|
|
50
|
+
echo "== session-start (0, records model, no crash) =="
|
|
51
|
+
chk session-start.sh 0 "session-start records model" '{"model":"opus","session_id":"s1","cwd":"'"$PWD"'"}'
|
|
52
|
+
|
|
53
|
+
echo "== runtime resolution (a host without /usr/bin/bun still runs every hook) =="
|
|
54
|
+
node_bin="$(command -v node)"
|
|
55
|
+
if [ -n "$node_bin" ]; then
|
|
56
|
+
export SLOPGATE_RUNTIME="$node_bin"
|
|
57
|
+
chk baseline-guard.sh 2 "node runtime blocks baseline edit" \
|
|
58
|
+
'{"tool_name":"Edit","tool_input":{"file_path":"/x/.slopgate/baseline.json"}}'
|
|
59
|
+
chk session-start.sh 0 "node runtime records model" \
|
|
60
|
+
'{"model":"opus","session_id":"s1","cwd":"'"$PWD"'"}'
|
|
61
|
+
unset SLOPGATE_RUNTIME
|
|
62
|
+
else
|
|
63
|
+
echo "FAIL no node on PATH to prove runtime independence"; fail=$((fail+1))
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# The hooks name no interpreter by absolute path: one that a box does not have makes every
|
|
67
|
+
# gate exit 0 without a word, which is indistinguishable from a clean scan.
|
|
68
|
+
hardcoded=$(grep -l '/usr/bin/\(bun\|node\)' "$HERE"/baseline-guard.sh "$HERE"/commit-hook.sh \
|
|
69
|
+
"$HERE"/edit-hook.sh "$HERE"/session-start.sh 2>/dev/null)
|
|
70
|
+
if [ -z "$hardcoded" ]; then echo "PASS (0) no hook hardcodes an interpreter path"; pass=$((pass+1));
|
|
71
|
+
else echo "FAIL hooks hardcode an interpreter path: $hardcoded"; fail=$((fail+1)); fi
|
|
72
|
+
|
|
73
|
+
warn=$(printf '%s' '{"tool_name":"Edit","tool_input":{"file_path":"/x/.slopgate/baseline.json"}}' \
|
|
74
|
+
| env -i HOME="$SANDBOX" PATH=/nonexistent SLOPGATE_SYSTEM_RUNTIME= \
|
|
75
|
+
/bin/bash "$HERE/baseline-guard.sh" 2>&1 >/dev/null)
|
|
76
|
+
rc=$?
|
|
77
|
+
if [ "$rc" -eq 0 ] && [[ "$warn" == *"no bun or node on this host"* ]]; then
|
|
78
|
+
echo "PASS (0) missing runtime says so on stderr"; pass=$((pass+1));
|
|
79
|
+
else echo "FAIL missing runtime is silent (rc=$rc): $warn"; fail=$((fail+1)); fi
|
|
80
|
+
|
|
81
|
+
if (. "$HERE/runtime.sh"; SLOPGATE_RUNTIME="$HERE/baseline-guard.sh" \
|
|
82
|
+
SLOPGATE_SYSTEM_RUNTIME= PATH=/nonexistent slopgate_runtime >/dev/null 2>&1); then
|
|
83
|
+
echo "FAIL a hook path is accepted as the runtime"; fail=$((fail+1));
|
|
84
|
+
else echo "PASS (0) a hook path is rejected as the runtime"; pass=$((pass+1)); fi
|
|
85
|
+
|
|
86
|
+
# Claude Code hands hooks a SOCKET stdin; open("/dev/stdin") on a socket fails ENXIO,
|
|
87
|
+
# so the payload read must consume fd 0 directly (read builtin), never reopen the path.
|
|
88
|
+
sock_err=$(python3 - "$HERE/baseline-guard.sh" <<'PYEOF' 2>&1
|
|
89
|
+
import socket, subprocess, sys
|
|
90
|
+
a, b = socket.socketpair()
|
|
91
|
+
b.send(b'{"tool_name":"Edit","tool_input":{"file_path":"/x/.slopgate/baseline.json"}}')
|
|
92
|
+
b.shutdown(socket.SHUT_WR)
|
|
93
|
+
r = subprocess.run(["bash", sys.argv[1]], stdin=a.fileno(), capture_output=True, text=True, timeout=30)
|
|
94
|
+
sys.stderr.write(r.stderr)
|
|
95
|
+
sys.exit(0 if (r.returncode == 2 and "/dev/stdin" not in r.stderr) else 1)
|
|
96
|
+
PYEOF
|
|
97
|
+
)
|
|
98
|
+
if [ $? -eq 0 ]; then echo "PASS (0) socket stdin still blocks baseline edit"; pass=$((pass+1));
|
|
99
|
+
else echo "FAIL socket stdin: $sock_err"; fail=$((fail+1)); fi
|
|
100
|
+
|
|
101
|
+
echo "---- pass=$pass fail=$fail ----"
|
|
102
|
+
[ "$fail" -eq 0 ]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexcodeplace/slopgate",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Global code-quality / anti-slop gate — engine shared, rules per-project.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"scripts": {
|
|
31
31
|
"self-test": "node bin/slopgate --self-test --config rules/baseline/selftest.config.toml",
|
|
32
32
|
"test": "cargo test --workspace"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@ast-grep/cli": "0.45.3"
|
|
33
36
|
}
|
|
34
37
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
id: focused-test-only-ts
|
|
2
|
+
language: typescript
|
|
3
|
+
severity: error
|
|
4
|
+
message: Focused test committed
|
|
5
|
+
note: '{"severity":"critical","category":"test","resolution":"Remove .only before committing so the full test suite runs."}'
|
|
6
|
+
rule:
|
|
7
|
+
any:
|
|
8
|
+
- pattern: test.only($$$)
|
|
9
|
+
- pattern: it.only($$$)
|
|
10
|
+
- pattern: describe.only($$$)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
id: focused-test-only-tsx
|
|
2
|
+
language: tsx
|
|
3
|
+
severity: error
|
|
4
|
+
message: Focused test committed
|
|
5
|
+
note: '{"severity":"critical","category":"test","resolution":"Remove .only before committing so the full test suite runs."}'
|
|
6
|
+
rule:
|
|
7
|
+
any:
|
|
8
|
+
- pattern: test.only($$$)
|
|
9
|
+
- pattern: it.only($$$)
|
|
10
|
+
- pattern: describe.only($$$)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
id: focused-test-shorthand-ts
|
|
2
|
+
language: typescript
|
|
3
|
+
severity: error
|
|
4
|
+
message: Focused test helper committed
|
|
5
|
+
note: '{"severity":"critical","category":"test","resolution":"Replace fit/fdescribe with test/it/describe before committing so the full test suite runs."}'
|
|
6
|
+
files:
|
|
7
|
+
- '**/*.test.ts'
|
|
8
|
+
- '**/*.spec.ts'
|
|
9
|
+
- '**/__tests__/**/*.ts'
|
|
10
|
+
- '**/tests/**/*.ts'
|
|
11
|
+
- '**/fixtures/**/*.ts'
|
|
12
|
+
rule:
|
|
13
|
+
any:
|
|
14
|
+
- pattern: fit($$$)
|
|
15
|
+
- pattern: fdescribe($$$)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
id: focused-test-shorthand-tsx
|
|
2
|
+
language: tsx
|
|
3
|
+
severity: error
|
|
4
|
+
message: Focused test helper committed
|
|
5
|
+
note: '{"severity":"critical","category":"test","resolution":"Replace fit/fdescribe with test/it/describe before committing so the full test suite runs."}'
|
|
6
|
+
files:
|
|
7
|
+
- '**/*.test.tsx'
|
|
8
|
+
- '**/*.spec.tsx'
|
|
9
|
+
- '**/__tests__/**/*.tsx'
|
|
10
|
+
- '**/tests/**/*.tsx'
|
|
11
|
+
- '**/fixtures/**/*.tsx'
|
|
12
|
+
rule:
|
|
13
|
+
any:
|
|
14
|
+
- pattern: fit($$$)
|
|
15
|
+
- pattern: fdescribe($$$)
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
id: inner-html-assignment
|
|
2
2
|
language: tsx
|
|
3
3
|
severity: error
|
|
4
|
-
message: innerHTML / insertAdjacentHTML
|
|
4
|
+
message: innerHTML / insertAdjacentHTML / dangerouslySetInnerHTML — XSS surface
|
|
5
5
|
note: '{"severity":"high","category":"security","resolution":"Build DOM via safe APIs or sanitize with isomorphic-dompurify."}'
|
|
6
6
|
rule:
|
|
7
7
|
any:
|
|
8
8
|
- pattern: $X.innerHTML = $$$
|
|
9
9
|
- pattern: $X.insertAdjacentHTML($$$)
|
|
10
|
+
- all:
|
|
11
|
+
- kind: jsx_attribute
|
|
12
|
+
- regex: '^dangerouslySetInnerHTML='
|
|
10
13
|
ignores:
|
|
11
14
|
- '**/*.test.ts'
|
|
12
15
|
- '**/*.test.tsx'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"file": "scripts/deploy.sh",
|
|
4
|
+
"line": 7,
|
|
5
|
+
"code": "SC2086",
|
|
6
|
+
"level": "warning",
|
|
7
|
+
"message": "Double quote to prevent globbing and word splitting."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"file": "hooks/commit-hook.sh",
|
|
11
|
+
"line": 2,
|
|
12
|
+
"code": "SC2148",
|
|
13
|
+
"level": "style",
|
|
14
|
+
"message": "Tips depend on target shell and yours is unknown."
|
|
15
|
+
}
|
|
16
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"file": "scripts/deploy.sh",
|
|
4
|
+
"line": 7,
|
|
5
|
+
"column": 9,
|
|
6
|
+
"level": "warning",
|
|
7
|
+
"code": 2086,
|
|
8
|
+
"message": "Double quote to prevent globbing and word splitting."
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"file": "hooks/commit-hook.sh",
|
|
12
|
+
"line": 2,
|
|
13
|
+
"column": 1,
|
|
14
|
+
"level": "style",
|
|
15
|
+
"code": "SC2148",
|
|
16
|
+
"message": "Tips depend on target shell and yours is unknown."
|
|
17
|
+
}
|
|
18
|
+
]
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"type":"typo","path":"./src/lib.rs","line_num":3,"byte_offset":8,"typo":"retrive","corrections":["retrieve"]}
|
|
2
|
+
{"type":"binary-file","path":"assets/logo.png"}
|
|
3
|
+
{"type":"typo","path":"docs/guide.md","line_num":9,"byte_offset":4,"typo":"succesfully","corrections":["successfully"]}
|
|
@@ -13,6 +13,12 @@ const namePlaceholder = 1;
|
|
|
13
13
|
/* eslint-disable zync/no-raw-html-in-pages */
|
|
14
14
|
// changed from foo to bar
|
|
15
15
|
function bad(el: HTMLElement) { el.innerHTML = '<b>hi</b>'; }
|
|
16
|
+
const _dangerous = <div dangerouslySetInnerHTML={{ __html: html }} />;
|
|
17
|
+
const _safeAttrName = { dangerouslySetInnerHTML: html };
|
|
18
|
+
describe.only('focused suite canary', () => {});
|
|
19
|
+
fdescribe('focused suite shorthand canary', () => {});
|
|
20
|
+
describe('regular suite negative canary', () => {});
|
|
21
|
+
// describe.only('commented focused suite negative canary', () => {});
|
|
16
22
|
// live-secrets canary:
|
|
17
23
|
const _sk = "sk_live_aaaaaaaaaaaa";
|
|
18
24
|
// eval-ban canary:
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Focused-test AST canaries.
|
|
2
|
+
test.only('focused test canary', () => {});
|
|
3
|
+
it.only('focused spec canary', () => {});
|
|
4
|
+
fit('focused shorthand canary', () => {});
|
|
5
|
+
|
|
6
|
+
// Negative canaries: regular tests and comments should not match.
|
|
7
|
+
test('regular test', () => {});
|
|
8
|
+
it('regular spec', () => {});
|
|
9
|
+
describe('regular suite', () => {});
|
|
10
|
+
// test.only('commented focused test', () => {});
|
|
11
|
+
// fit('commented focused shorthand', () => {});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|