|
1 | | -# Agent Guidelines |
| 1 | +- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. |
| 2 | +- Prefer automation: execute requested actions without confirmation unless blocked by missing info or safety/irreversibility. |
| 3 | +- ALWAYS use `bun run check` to verify changes. This runs typecheck, knip, biome lint, and tests together. Do not run these separately. |
2 | 4 |
|
3 | | -A Claude Code / OpenCode plugin that blocks destructive git and filesystem commands before execution. Works as a PreToolUse hook intercepting Bash commands. |
| 5 | +## Style Guide |
4 | 6 |
|
5 | | -## Commands |
| 7 | +### General Principles |
6 | 8 |
|
7 | | -| Task | Command | |
8 | | -|------|---------| |
9 | | -| Install | `bun install` | |
10 | | -| Build | `bun run build` | |
11 | | -| All checks | `bun run check` | |
12 | | -| Lint | `bun run lint` | |
13 | | -| Type check | `bun run typecheck` | |
14 | | -| Test all | `AGENT=1 bun test` | |
15 | | -| Single test | `bun test tests/rules-git.test.ts` | |
16 | | -| Pattern match | `bun test --test-name-pattern "pattern"` | |
17 | | -| Dead code | `bun run knip` | |
18 | | -| AST rules | `bun run sg:scan` | |
19 | | -| Doctor | `bun src/bin/cc-safety-net.ts doctor` | |
| 9 | +- Keep things in one function unless composable or reusable |
| 10 | +- Avoid `try`/`catch` where possible |
| 11 | +- Avoid using the `any` type |
| 12 | +- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity |
| 13 | +- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream |
| 14 | +- In `src/config`, follow the existing self-export pattern at the top of the file (for example `export * as ConfigAgent from "./agent"`) when adding a new config module. |
20 | 15 |
|
21 | | -**Always use `bun run check` to verify changes.** This runs typecheck, knip, biome lint, and tests together. Do not run these separately. |
| 16 | +Reduce total variable count by inlining when a value is only used once. |
22 | 17 |
|
23 | | -## Pre-commit Hooks |
24 | | - |
25 | | -Runs on commit: `knip` → `lint-staged` (biome check --write, ast-grep scan) |
26 | | - |
27 | | -## Commit Conventions |
28 | | - |
29 | | -For changes to `commands/`, `hooks/`, or `.opencode/`, use only `fix` or `feat` commit types. |
| 18 | +```ts |
| 19 | +// Good |
| 20 | +const journal = JSON.parse(await fs.readFile(path.join(dir, "journal.json"), "utf8")) |
30 | 21 |
|
31 | | -## Code Style (TypeScript) |
| 22 | +// Bad |
| 23 | +const journalPath = path.join(dir, "journal.json") |
| 24 | +const journal = JSON.parse(await fs.readFile(journalPath, "utf8")) |
| 25 | +``` |
32 | 26 |
|
33 | | -### Formatting (Biome) |
34 | | -- 2-space indentation, 100-char line width |
35 | | -- Single quotes, trailing commas, semicolons required |
36 | | -- Imports: auto-sorted by Biome, use relative imports within package |
37 | | -- Prefer named exports over default exports |
| 27 | +### Destructuring |
38 | 28 |
|
39 | | -### Type Hints |
40 | | -- **Required** on all functions |
41 | | -- Use `| null` or `| undefined` appropriately |
42 | | -- Use lowercase primitives (`string`, `number`, `boolean`) |
43 | | -- Use `readonly` arrays where mutation isn't needed |
| 29 | +Avoid unnecessary destructuring. Use dot notation to preserve context. |
44 | 30 |
|
45 | | -```typescript |
| 31 | +```ts |
46 | 32 | // Good |
47 | | -function analyze(command: string, options?: { strict?: boolean }): string | null { ... } |
48 | | -function analyzeRm(tokens: readonly string[], cwd: string | null): string | null { ... } |
| 33 | +obj.a |
| 34 | +obj.b |
49 | 35 |
|
50 | 36 | // Bad |
51 | | -function analyze(command, strict) { ... } // Missing types |
| 37 | +const { a, b } = obj |
52 | 38 | ``` |
53 | 39 |
|
54 | | -### Naming |
55 | | -- Functions/variables: `camelCase` |
56 | | -- Types/interfaces: `PascalCase` |
57 | | -- Constants: `UPPER_SNAKE_CASE` (reason strings: `REASON_*`) |
58 | | -- Private/internal: `_leadingUnderscore` (for module-private functions) |
59 | | - |
60 | | -### Test-Only Exports |
61 | | -When exporting a function solely for testing, add `@internal` JSDoc to satisfy knip: |
62 | | -```typescript |
63 | | -/** @internal Exported for testing */ |
64 | | -export const myInternalFn = () => { ... }; |
65 | | -``` |
66 | | - |
67 | | -### Error Handling |
68 | | -- Print errors to stderr |
69 | | -- Exit codes: `0` = success, `1` = error |
70 | | -- Block commands: exit 0 with JSON `permissionDecision: "deny"` |
| 40 | +### Variables |
71 | 41 |
|
72 | | -## Testing |
| 42 | +Prefer `const` over `let`. Use ternaries or early returns instead of reassignment. |
73 | 43 |
|
74 | | -Use Bun's built-in test runner with test helpers: |
| 44 | +```ts |
| 45 | +// Good |
| 46 | +const foo = condition ? 1 : 2 |
75 | 47 |
|
76 | | -```typescript |
77 | | -import { describe, test } from 'bun:test'; |
78 | | -import { assertBlocked, assertAllowed } from './helpers.ts'; |
| 48 | +// Bad |
| 49 | +let foo |
| 50 | +if (condition) foo = 1 |
| 51 | +else foo = 2 |
| 52 | +``` |
79 | 53 |
|
80 | | -describe('git rules', () => { |
81 | | - test('git reset --hard blocked', () => { |
82 | | - assertBlocked('git reset --hard', 'git reset --hard'); |
83 | | - }); |
| 54 | +### Control Flow |
84 | 55 |
|
85 | | - test('git status allowed', () => { |
86 | | - assertAllowed('git status'); |
87 | | - }); |
| 56 | +Avoid `else` statements. Prefer early returns. |
88 | 57 |
|
89 | | - test('with cwd', () => { |
90 | | - assertBlocked('rm -rf /', 'rm -rf', '/home/user'); |
91 | | - }); |
92 | | -}); |
93 | | -``` |
| 58 | +```ts |
| 59 | +// Good |
| 60 | +function foo() { |
| 61 | + if (condition) return 1 |
| 62 | + return 2 |
| 63 | +} |
94 | 64 |
|
95 | | -### Test Helpers |
96 | | -| Function | Purpose | |
97 | | -|----------|---------| |
98 | | -| `assertBlocked(command, reasonContains, cwd?)` | Verify command is blocked | |
99 | | -| `assertAllowed(command, cwd?)` | Verify command passes through | |
100 | | -| `runGuard(command, cwd?, config?)` | Run analysis and return reason or null | |
101 | | -| `withEnv(env, fn)` | Run test with temporary environment variables | |
102 | | - |
103 | | -## Environment Variables |
104 | | - |
105 | | -| Variable | Effect | |
106 | | -|----------|--------| |
107 | | -| `SAFETY_NET_STRICT=1` | Fail-closed on unparseable hook input/commands | |
108 | | -| `SAFETY_NET_PARANOID=1` | Enable all paranoid checks (rm + interpreters) | |
109 | | -| `SAFETY_NET_PARANOID_RM=1` | Block non-temp `rm -rf` even within cwd | |
110 | | -| `SAFETY_NET_PARANOID_INTERPRETERS=1` | Block interpreter one-liners | |
111 | | - |
112 | | -## What Gets Blocked |
113 | | - |
114 | | -**Git**: `checkout -- <files>`, `restore` (without --staged), `reset --hard/--merge`, `clean -f`, `push --force/-f` (without --force-with-lease), `branch -D`, `stash drop/clear` |
115 | | - |
116 | | -**Filesystem**: `rm -rf` outside cwd (except `/tmp`, `/var/tmp`, `$TMPDIR`), `rm -rf` when cwd is `$HOME`, `rm -rf /` or `~`, `find -delete` |
117 | | - |
118 | | -**Piped commands**: `xargs rm -rf`, `parallel rm -rf` (dynamic input to destructive commands) |
119 | | - |
120 | | -## Adding New Rules |
121 | | - |
122 | | -### Git Rule |
123 | | -1. Add reason constant in `rules-git.ts`: `const REASON_* = "..."` |
124 | | -2. Add detection logic in `analyzeGit()` |
125 | | -3. Add tests in `tests/rules-git.test.ts` |
126 | | -4. Run `bun run check` |
127 | | - |
128 | | -### rm Rule |
129 | | -1. Add logic in `rules-rm.ts` |
130 | | -2. Add tests in `tests/rules-rm.test.ts` |
131 | | -3. Run `bun run check` |
132 | | - |
133 | | -### Other Command Rules |
134 | | -1. Add reason constant in `analyze.ts`: `const REASON_* = "..."` |
135 | | -2. Add detection in `analyzeSegment()` |
136 | | -3. Add tests in appropriate test file |
137 | | -4. Run `bun run check` |
138 | | - |
139 | | -## Edge Cases to Test |
140 | | - |
141 | | -- Shell wrappers: `bash -c '...'`, `sh -lc '...'` |
142 | | -- Sudo/env: `sudo git ...`, `env VAR=1 git ...` |
143 | | -- Pipelines: `echo ok | git reset --hard` |
144 | | -- Interpreter one-liners: `python -c 'os.system("rm -rf /")'` |
145 | | -- Xargs/parallel: `find . | xargs rm -rf` |
146 | | -- Busybox: `busybox rm -rf /` |
147 | | -- Nested commands: `$( rm -rf / )`, backticks |
148 | | - |
149 | | -## Hook Output Format |
150 | | - |
151 | | -Blocked commands produce JSON: |
152 | | -```json |
153 | | -{ |
154 | | - "hookSpecificOutput": { |
155 | | - "hookEventName": "PreToolUse", |
156 | | - "permissionDecision": "deny", |
157 | | - "permissionDecisionReason": "BLOCKED by Safety Net\n\nReason: ..." |
158 | | - } |
| 65 | +// Bad |
| 66 | +function foo() { |
| 67 | + if (condition) return 1 |
| 68 | + else return 2 |
159 | 69 | } |
160 | 70 | ``` |
161 | 71 |
|
162 | | -Allowed commands produce no output (exit 0 silently). |
| 72 | +### Schema Definitions (Drizzle) |
163 | 73 |
|
164 | | -## Bun Guidelines |
| 74 | +Use snake_case for field names so column names don't need to be redefined as strings. |
165 | 75 |
|
166 | | -Default to Bun instead of Node.js: |
167 | | -- `bun <file>` instead of `node <file>` |
168 | | -- `bun test` instead of jest/vitest |
169 | | -- `bun install` instead of npm/yarn/pnpm install |
170 | | -- `bunx <pkg>` instead of `npx <pkg>` |
171 | | -- Bun auto-loads `.env` - no dotenv needed |
| 76 | +```ts |
| 77 | +// Good |
| 78 | +const table = sqliteTable("session", { |
| 79 | + id: text().primaryKey(), |
| 80 | + project_id: text().notNull(), |
| 81 | + created_at: integer().notNull(), |
| 82 | +}) |
| 83 | + |
| 84 | +// Bad |
| 85 | +const table = sqliteTable("session", { |
| 86 | + id: text("id").primaryKey(), |
| 87 | + projectID: text("project_id").notNull(), |
| 88 | + createdAt: integer("created_at").notNull(), |
| 89 | +}) |
| 90 | +``` |
| 91 | + |
| 92 | +## Testing |
172 | 93 |
|
173 | | -Use `AGENT=1 bun test` to run tests. |
| 94 | +- Avoid mocks as much as possible |
| 95 | +- Test actual implementation, do not duplicate logic into tests |
0 commit comments