From 56b7210c27255366a3930f4477f0d9b8a0ec1991 Mon Sep 17 00:00:00 2001 From: kenryu42 Date: Mon, 4 May 2026 19:01:11 +0900 Subject: [PATCH 01/16] docs: update agent guidelines with style guide conventions Rewrite AGENTS.md from project-specific documentation to comprehensive style guide covering destructuring, variables, control flow, schema definitions, and testing conventions. Simplify CLAUDE.md to reference AGENTS.md instead of duplicating content. --- AGENTS.md | 214 +++++++++++++++++------------------------------------- CLAUDE.md | 119 +----------------------------- 2 files changed, 69 insertions(+), 264 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fd22b3e..2f2cedb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,173 +1,95 @@ -# Agent Guidelines +- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. +- Prefer automation: execute requested actions without confirmation unless blocked by missing info or safety/irreversibility. +- ALWAYS use `bun run check` to verify changes. This runs typecheck, knip, biome lint, and tests together. Do not run these separately. -A Claude Code / OpenCode plugin that blocks destructive git and filesystem commands before execution. Works as a PreToolUse hook intercepting Bash commands. +## Style Guide -## Commands +### General Principles -| Task | Command | -|------|---------| -| Install | `bun install` | -| Build | `bun run build` | -| All checks | `bun run check` | -| Lint | `bun run lint` | -| Type check | `bun run typecheck` | -| Test all | `AGENT=1 bun test` | -| Single test | `bun test tests/rules-git.test.ts` | -| Pattern match | `bun test --test-name-pattern "pattern"` | -| Dead code | `bun run knip` | -| AST rules | `bun run sg:scan` | -| Doctor | `bun src/bin/cc-safety-net.ts doctor` | +- Keep things in one function unless composable or reusable +- Avoid `try`/`catch` where possible +- Avoid using the `any` type +- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity +- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream +- 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. -**Always use `bun run check` to verify changes.** This runs typecheck, knip, biome lint, and tests together. Do not run these separately. +Reduce total variable count by inlining when a value is only used once. -## Pre-commit Hooks - -Runs on commit: `knip` → `lint-staged` (biome check --write, ast-grep scan) - -## Commit Conventions - -For changes to `commands/`, `hooks/`, or `.opencode/`, use only `fix` or `feat` commit types. +```ts +// Good +const journal = JSON.parse(await fs.readFile(path.join(dir, "journal.json"), "utf8")) -## Code Style (TypeScript) +// Bad +const journalPath = path.join(dir, "journal.json") +const journal = JSON.parse(await fs.readFile(journalPath, "utf8")) +``` -### Formatting (Biome) -- 2-space indentation, 100-char line width -- Single quotes, trailing commas, semicolons required -- Imports: auto-sorted by Biome, use relative imports within package -- Prefer named exports over default exports +### Destructuring -### Type Hints -- **Required** on all functions -- Use `| null` or `| undefined` appropriately -- Use lowercase primitives (`string`, `number`, `boolean`) -- Use `readonly` arrays where mutation isn't needed +Avoid unnecessary destructuring. Use dot notation to preserve context. -```typescript +```ts // Good -function analyze(command: string, options?: { strict?: boolean }): string | null { ... } -function analyzeRm(tokens: readonly string[], cwd: string | null): string | null { ... } +obj.a +obj.b // Bad -function analyze(command, strict) { ... } // Missing types +const { a, b } = obj ``` -### Naming -- Functions/variables: `camelCase` -- Types/interfaces: `PascalCase` -- Constants: `UPPER_SNAKE_CASE` (reason strings: `REASON_*`) -- Private/internal: `_leadingUnderscore` (for module-private functions) - -### Test-Only Exports -When exporting a function solely for testing, add `@internal` JSDoc to satisfy knip: -```typescript -/** @internal Exported for testing */ -export const myInternalFn = () => { ... }; -``` - -### Error Handling -- Print errors to stderr -- Exit codes: `0` = success, `1` = error -- Block commands: exit 0 with JSON `permissionDecision: "deny"` +### Variables -## Testing +Prefer `const` over `let`. Use ternaries or early returns instead of reassignment. -Use Bun's built-in test runner with test helpers: +```ts +// Good +const foo = condition ? 1 : 2 -```typescript -import { describe, test } from 'bun:test'; -import { assertBlocked, assertAllowed } from './helpers.ts'; +// Bad +let foo +if (condition) foo = 1 +else foo = 2 +``` -describe('git rules', () => { - test('git reset --hard blocked', () => { - assertBlocked('git reset --hard', 'git reset --hard'); - }); +### Control Flow - test('git status allowed', () => { - assertAllowed('git status'); - }); +Avoid `else` statements. Prefer early returns. - test('with cwd', () => { - assertBlocked('rm -rf /', 'rm -rf', '/home/user'); - }); -}); -``` +```ts +// Good +function foo() { + if (condition) return 1 + return 2 +} -### Test Helpers -| Function | Purpose | -|----------|---------| -| `assertBlocked(command, reasonContains, cwd?)` | Verify command is blocked | -| `assertAllowed(command, cwd?)` | Verify command passes through | -| `runGuard(command, cwd?, config?)` | Run analysis and return reason or null | -| `withEnv(env, fn)` | Run test with temporary environment variables | - -## Environment Variables - -| Variable | Effect | -|----------|--------| -| `SAFETY_NET_STRICT=1` | Fail-closed on unparseable hook input/commands | -| `SAFETY_NET_PARANOID=1` | Enable all paranoid checks (rm + interpreters) | -| `SAFETY_NET_PARANOID_RM=1` | Block non-temp `rm -rf` even within cwd | -| `SAFETY_NET_PARANOID_INTERPRETERS=1` | Block interpreter one-liners | - -## What Gets Blocked - -**Git**: `checkout -- `, `restore` (without --staged), `reset --hard/--merge`, `clean -f`, `push --force/-f` (without --force-with-lease), `branch -D`, `stash drop/clear` - -**Filesystem**: `rm -rf` outside cwd (except `/tmp`, `/var/tmp`, `$TMPDIR`), `rm -rf` when cwd is `$HOME`, `rm -rf /` or `~`, `find -delete` - -**Piped commands**: `xargs rm -rf`, `parallel rm -rf` (dynamic input to destructive commands) - -## Adding New Rules - -### Git Rule -1. Add reason constant in `rules-git.ts`: `const REASON_* = "..."` -2. Add detection logic in `analyzeGit()` -3. Add tests in `tests/rules-git.test.ts` -4. Run `bun run check` - -### rm Rule -1. Add logic in `rules-rm.ts` -2. Add tests in `tests/rules-rm.test.ts` -3. Run `bun run check` - -### Other Command Rules -1. Add reason constant in `analyze.ts`: `const REASON_* = "..."` -2. Add detection in `analyzeSegment()` -3. Add tests in appropriate test file -4. Run `bun run check` - -## Edge Cases to Test - -- Shell wrappers: `bash -c '...'`, `sh -lc '...'` -- Sudo/env: `sudo git ...`, `env VAR=1 git ...` -- Pipelines: `echo ok | git reset --hard` -- Interpreter one-liners: `python -c 'os.system("rm -rf /")'` -- Xargs/parallel: `find . | xargs rm -rf` -- Busybox: `busybox rm -rf /` -- Nested commands: `$( rm -rf / )`, backticks - -## Hook Output Format - -Blocked commands produce JSON: -```json -{ - "hookSpecificOutput": { - "hookEventName": "PreToolUse", - "permissionDecision": "deny", - "permissionDecisionReason": "BLOCKED by Safety Net\n\nReason: ..." - } +// Bad +function foo() { + if (condition) return 1 + else return 2 } ``` -Allowed commands produce no output (exit 0 silently). +### Schema Definitions (Drizzle) -## Bun Guidelines +Use snake_case for field names so column names don't need to be redefined as strings. -Default to Bun instead of Node.js: -- `bun ` instead of `node ` -- `bun test` instead of jest/vitest -- `bun install` instead of npm/yarn/pnpm install -- `bunx ` instead of `npx ` -- Bun auto-loads `.env` - no dotenv needed +```ts +// Good +const table = sqliteTable("session", { + id: text().primaryKey(), + project_id: text().notNull(), + created_at: integer().notNull(), +}) + +// Bad +const table = sqliteTable("session", { + id: text("id").primaryKey(), + projectID: text("project_id").notNull(), + createdAt: integer("created_at").notNull(), +}) +``` + +## Testing -Use `AGENT=1 bun test` to run tests. +- Avoid mocks as much as possible +- Test actual implementation, do not duplicate logic into tests diff --git a/CLAUDE.md b/CLAUDE.md index ffd2c7e..eef4bd2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,118 +1 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -A Claude Code and OpenCode plugin that blocks destructive git and filesystem commands before execution. It works as a PreToolUse hook that intercepts Bash commands and denies dangerous operations like `git reset --hard`, `rm -rf`, and `git checkout -- `. - -## Commands - -- **Setup**: `bun install` -- **All checks**: `bun run check` (runs lint, typecheck, knip, ast-grep scan, tests) -- **Single test**: `bun test tests/file.test.ts` -- **Test pattern**: `bun test --test-name-pattern "pattern"` -- **Lint**: `bun run lint` (uses Biome) -- **Type check**: `bun run typecheck` -- **Dead code**: `bun run knip` -- **AST scan**: `bun run sg:scan` -- **Build**: `bun run build` -- **Doctor**: `bun src/bin/cc-safety-net.ts doctor` (diagnostics) - -**Always use `bun run check` to verify changes.** This runs typecheck, knip, biome lint, and tests together. Do not run these separately. - -## Pre-commit Hooks - -Runs on commit: `knip` → `lint-staged` (biome check --write, ast-grep scan) - -## Slash Commands - -- `/set-statusline`: Configure status line integration -- `/set-custom-rules`: Create custom rules interactively -- `/verify-custom-rules`: Validate custom rules config - -## Architecture - -The hook receives JSON input on stdin containing `tool_name` and `tool_input`. For `Bash` tools, it analyzes the command and outputs JSON with `permissionDecision: "deny"` to block dangerous operations. - -**Entry points**: -- `src/bin/cc-safety-net.ts` — Claude Code CLI (reads stdin JSON) -- `src/index.ts` — OpenCode plugin export - -**Core analysis flow**: -1. `cc-safety-net.ts:main()` parses JSON input, extracts command -2. `analyze.ts:analyzeCommand()` splits command on shell operators (`;`, `&&`, `|`, etc.) -3. `analyzeSegment()` tokenizes each segment, strips wrappers (sudo, env), identifies the command -4. Dispatches to `rules-git.ts:analyzeGit()` or `rules-rm.ts:analyzeRm()` based on command -5. Checks custom rules via `rules-custom.ts:checkCustomRules()` if configured - -**Key modules** (`src/core/`): -- `shell.ts`: Shell parsing (`splitShellCommands`, `shlexSplit`, `stripWrappers`, `shortOpts`) -- `rules-git.ts`: Git subcommand analysis (checkout, restore, reset, clean, push, branch, stash) -- `rules-rm.ts`: rm analysis (allows rm -rf within cwd except when cwd is $HOME; temp paths always allowed; strict mode blocks non-temp) -- `config.ts`: Config loading, validation, merging (user `~/.cc-safety-net/config.json` + project `.safety-net.json`) -- `rules-custom.ts`: Custom rule matching (`checkCustomRules()`) -- `audit.ts`: Audit logging for blocked commands -- `verify-config.ts`: Config validator - -**Analysis submodules** (`src/core/analyze/`): -- `find.ts`: `find -delete` and `find -exec rm` detection -- `interpreters.ts`: Python/Node/Ruby/Perl one-liner detection -- `xargs.ts`: `xargs rm` and dynamic input detection -- `parallel.ts`: GNU parallel command analysis -- `shell-wrappers.ts`: Recursive `bash -c`/`sh -c` unwrapping -- `tmpdir.ts`: Temp directory path detection - -**Test utilities** (`tests/helpers.ts`): -- `assertBlocked()`, `assertAllowed()` helpers for testing command analysis - -**Advanced detection**: -- Recursively analyzes shell wrappers (`bash -c '...'`) up to 5 levels deep -- Detects destructive commands in interpreter one-liners (`python -c`, `node -e`, `ruby -e`, `perl -e`) -- Handles `xargs` and `parallel` with template expansion and dynamic input detection -- Detects `find -delete` and `find -exec rm` patterns -- Redacts secrets (tokens, passwords, API keys) in block messages and audit logs -- Audit logging: blocked commands logged to `~/.cc-safety-net/logs/.jsonl` - -## Code Style (TypeScript) - -- Use Bun instead of Node.js for running, testing, and building -- Biome for linting and formatting -- All functions require type annotations -- Use `type | null` syntax (not `undefined` where possible) -- Use kebab-case for file names (`rules-git.ts`, not `rulesGit.ts`) -- For test-only exports, add `/** @internal Exported for testing */` JSDoc to satisfy knip - -## Commit Conventions - -When committing changes to files in `commands/`, `hooks/`, or `.opencode/`, use only `fix` or `feat` commit types. These directories contain user-facing skill definitions and hook configurations that represent features or fixes to the plugin's capabilities. - -## Environment Variables - -- `SAFETY_NET_STRICT=1`: Strict mode (fail-closed on unparseable hook input/commands) -- `SAFETY_NET_PARANOID=1`: Paranoid mode (enables all paranoid checks) -- `SAFETY_NET_PARANOID_RM=1`: Paranoid rm (blocks non-temp `rm -rf` even within cwd) -- `SAFETY_NET_PARANOID_INTERPRETERS=1`: Paranoid interpreters (blocks interpreter one-liners) - -## Custom Rules - -Users can define additional blocking rules in two scopes (merged, project overrides user): -- **User scope**: `~/.cc-safety-net/config.json` (applies to all projects) -- **Project scope**: `.safety-net.json` (in project root) - -Rules are additive only—cannot bypass built-in protections. Invalid config silently falls back to built-in rules only. - -## Testing - -Use `AGENT=1 bun test` to run tests. - -## Bun Best Practices - -- Use `bun ` instead of `node ` or `ts-node ` -- Use `bun test` instead of `jest` or `vitest` -- Use `bun build` instead of `webpack` or `esbuild` -- Use `bun install` instead of `npm install` -- Use `bun run