Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "clawband"
version = "3.18.1"
version = "3.19.2"

edition = "2021"
description = "Claude Code PreToolUse hook that guards against destructive shell commands and unsafe file-write content"
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ const msg = "eval(x) is dangerous"; // clawband: ignored — this is a string li
result = eval(userInput); // clawband: flagged — this is a real call expression
```

**Status**: seven rules across all 4 supported languages (Rust, Python, JavaScript, TypeScript) — a growing set, not yet a complete one:
**Status**: eight rules across all 4 supported languages (Rust, Python, JavaScript, TypeScript) — a growing set, not yet a complete one:

- `dynamic-eval`: `eval()`/`Function()` in JS/TS, `eval()`/`exec()` in Python (no Rust equivalent — there's no direct analog to construct-code-from-a-string in safe Rust)
- `shell-invoking-subprocess`: handing a string to a shell interpreter instead of exec'ing a program directly — `subprocess.run/call/Popen/check_call/check_output(..., shell=True)` and bare `os.system`/`os.popen` in Python; `.exec`/`.execSync` in JS/TS (`.execFile`/`.spawn` and their `*Sync` variants are deliberately not flagged — they take an argv array and never invoke a shell); `Command::new("sh"/"bash"/"/bin/sh"/"/bin/bash").arg("-c")` in Rust
Expand All @@ -308,6 +308,7 @@ result = eval(userInput); // clawband: flagged — this is a real call expressio
- `dynamic-module-load`: `require(...)`/dynamic `import(...)` in JS/TS whose argument isn't a fixed string literal — the "flag everything except a specific node kind" shape, so like the `Loader=` case above it needed a Rust-side check of the argument node's kind rather than a pure query; a template literal with `${...}` interpolation counts as non-literal and flags (the exact i18n-loader-built-from-a-request-param bug class this rule targets), but a plain template literal with no interpolation is treated as literal-equivalent and doesn't (Python/Rust have no v1 equivalent — `importlib.import_module` is a deliberate v2 follow-up)
- `sql-string-interpolation`: a query-execution call whose argument is built via string interpolation/concatenation/formatting instead of passed as a separate parameter — the structural shape of SQL injection, independent of whether the interpolated value is actually attacker-controlled. **Higher false-positive risk than the other rules** — flagged for extra review in its PR. `.execute`/`.executemany` in Python (covers `sqlite3`/`psycopg2`/`pymysql`/SQLAlchemy raw-connection calls, matched by method-name suffix, not object name) where the argument is an f-string with real `{...}` interpolation, a `%`-format or `.format()` call, or `+`-concatenation — a parameterized call (`execute("...?", (val,))`) or an f-string with zero interpolations both correctly fall through as safe, same argument-node-kind-inspection approach as `dynamic-module-load`; `.query`/`.execute` in JS/TS where the argument is a template literal containing `${...}` interpolation (no Rust equivalent for v1 — sqlx's compile-time-checked macros and diesel's query builder make this a much rarer footgun there)
- `rust-unsafe-block`: the odd one out — `unsafe { ... }` isn't inherently a bug the way the rules above are; it's a routine, sometimes-necessary tool (FFI, raw pointer manipulation for performance). The value is *visibility*, making sure an agent-written `unsafe` block gets a human's eyes on it, not "this is always wrong." Rust only, a direct `(unsafe_block)` match with no predicate needed; a bare `unsafe fn` signature with no `{ }` body is a distinct grammar node (`function_modifiers`, no `unsafe_block` child) and is deliberately out of scope for v1
- `xss-sink`: browser/React APIs that render their argument as live HTML/script rather than as inert text — JS/TS only. `.innerHTML =`/`.outerHTML =` assignment (a `member_expression` assignment *target*, a different query shape than the call-expression rules above), `.insertAdjacentHTML(...)`, and `document.write(...)` are matched unconditionally, with no attempt to tell a "safe-looking" static-string assignment from a dynamic one — the upstream reference this rule targets parity with (the `security-guidance` Claude Code plugin's `innerHTML_xss`/`outerHTML_xss`/`insertAdjacentHTML_xss`/`document_write_xss` rules) doesn't narrow on value either, just on file extension, so there's no narrower behavior to reproduce here. React's `dangerouslySetInnerHTML` JSX attribute is also flagged, but only where the grammar can actually parse JSX: `tree-sitter-javascript`'s default grammar parses JSX out of the box (so `.js`/`.jsx` are covered), and `.tsx` needed a second, dedicated TypeScript grammar variant (`tree_sitter_typescript::LANGUAGE_TSX`, alongside the existing `LANGUAGE_TYPESCRIPT` used for plain `.ts`) since confirmed empirically that `LANGUAGE_TYPESCRIPT` has zero JSX node kinds at all — a `.ts` file can't contain JSX syntax, so this sub-rule doesn't apply there

Unsupported languages fail open: the path-based checks above still apply, this only ever adds coverage, it never removes it. Runs in the same `PreToolUse` handler as the path checks — deny (self-protection, protect.paths) always takes priority over an AST-guard `ask`, since the handler returns as soon as an earlier, higher-severity check fires.

Expand Down Expand Up @@ -553,7 +554,7 @@ Removing the entry from that list lets clawband's ask/deny patterns take over as
- **Force-push gaps** — `git push :<branch>` (colon-prefix deletion) and `git push origin +main` (plus-refspec force) are not blocked by the force-push pattern; use `--delete` / `--force-with-lease` instead.
- **Commit messages containing blocked patterns** — if a commit message itself contains a pattern like `rm -rf /` (e.g. documenting a fix), clawband will block the `git commit` command. Workaround: write the message to a temp file and use `git commit -F /tmp/msg.txt`, or rephrase to avoid the literal pattern.
- **Fail-closed on parse error** — if clawband cannot read or parse the hook input (stdin read failure, malformed JSON), it emits `deny` and blocks the command. It does **not** fail-open.
- **AST content guard covers seven rules so far** — `dynamic-eval` (`eval`/`Function`/`exec`), `shell-invoking-subprocess`, `insecure-deserialize`, `tls-verify-disabled`, `dynamic-module-load`, `sql-string-interpolation`, and `rust-unsafe-block` (see [AST content guard](#ast-content-guard) above); any language outside Rust/Python/JS/TS falls open. This augments, not replaces, path-based Write/Edit protection.
- **AST content guard covers eight rules so far** — `dynamic-eval` (`eval`/`Function`/`exec`), `shell-invoking-subprocess`, `insecure-deserialize`, `tls-verify-disabled`, `dynamic-module-load`, `sql-string-interpolation`, `rust-unsafe-block`, and `xss-sink` (see [AST content guard](#ast-content-guard) above); any language outside Rust/Python/JS/TS falls open. This augments, not replaces, path-based Write/Edit protection.

### Known shell obfuscation bypasses (issue #129)

Expand Down
Loading
Loading