Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,6 @@ bun run check
- [ ] Code follows project conventions (type hints, naming, etc.)
- [ ] `bun run check` passes (lint, types, dead code, rules, tests)
- [ ] Tests added for new rules (minimum 90% coverage required)
- [ ] Tested locally with Claude Code, OpenCode, Gemini CLI or GitHub Copilot CLI
- [ ] Tested locally with Claude Code, OpenCode, Gemini CLI, GitHub Copilot CLI or Codex
- [ ] Updated documentation if needed (README, AGENTS.md)
- [ ] No version changes in `package.json`
214 changes: 68 additions & 146 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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 -- <files>`, `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 <file>` instead of `node <file>`
- `bun test` instead of jest/vitest
- `bun install` instead of npm/yarn/pnpm install
- `bunx <pkg>` instead of `npx <pkg>`
- 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
119 changes: 1 addition & 118 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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 -- <files>`.

## 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/<session_id>.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 <file>` instead of `node <file>` or `ts-node <file>`
- 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 <script>` instead of `npm run <script>`
- Bun automatically loads .env, so don't use dotenv

For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
@AGENTS.md
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,9 @@ claude-code-safety-net/
│ ├── rules/ # AST-grep rule definitions
│ ├── rule-tests/ # Rule test cases
│ └── utils/ # Shared utilities
├── commands/
│ ├── set-custom-rules.md # Slash command: configure custom rules
│ └── verify-custom-rules.md # Slash command: validate config
├── skills/
│ ├── set-custom-rules/ # Skill: configure custom rules
│ └── verify-custom-rules/ # Skill: validate config
├── hooks/
│ └── hooks.json # Hook definitions
├── scripts/
Expand Down
Loading
Loading