Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
019ea07
init attempt on guardrail interface
Reapor-Yurnero Feb 1, 2026
1ae9c11
fix some typing errors
Reapor-Yurnero Feb 1, 2026
4a6da7a
fix bug that would hang on pre request violation indefinitely
Reapor-Yurnero Feb 1, 2026
84055e5
fix web browser not seeing blocked message bug
Reapor-Yurnero Feb 1, 2026
6b0d748
fix an error from the previous fix
Reapor-Yurnero Feb 1, 2026
6d7f989
add some doc
Reapor-Yurnero Feb 1, 2026
85ab3a2
refactor: migrate guardrails to plugin hook system
Scrattlebeard Feb 1, 2026
5c74c85
Add LlamaGuard and gpt-oss-safeguard plugins. Remove guardrails confi…
Scrattlebeard Feb 1, 2026
7ff2716
refactor: add createGuardrailPlugin factory for unified guardrail API
Scrattlebeard Feb 1, 2026
965dbc0
feat: add command-safety-guard and security-audit plugins
Scrattlebeard Feb 1, 2026
fd6b621
Plugins: fix guardrail hook sequencing and short circuiting
Reapor-Yurnero Feb 2, 2026
e2fd8a1
change plugin naming to follow conventions
Reapor-Yurnero Feb 2, 2026
efc7065
remove redundant config enabled key
Reapor-Yurnero Feb 2, 2026
f646657
revert some unnecesary validation after moving to plugins
Reapor-Yurnero Feb 2, 2026
d1e1e31
remove redundant config.enable schema
Reapor-Yurnero Feb 2, 2026
6b5719b
Guardrails: GPT-OSS safeguard defaults and append mode
Reapor-Yurnero Feb 2, 2026
ce2a526
remove llama guard for the moment
Reapor-Yurnero Feb 2, 2026
211dde8
Guardrails: skip hooks for internal runs
Reapor-Yurnero Feb 2, 2026
c2bdd7d
polish the doc
Reapor-Yurnero Feb 2, 2026
1bb5876
Guardrails: add priority and attribution
Reapor-Yurnero Feb 2, 2026
a646c9a
Enhance command-safety-guard and security-audit plugins with UI hints…
Scrattlebeard Feb 2, 2026
70edfa0
Merge branch 'feat/guardrail_interface' into bugfix/command-safety-an…
Scrattlebeard Feb 2, 2026
87c6051
Fix implementations to use the correct tool names.
Scrattlebeard Feb 2, 2026
664fdcc
Merge pull request #5 from grayswansecurity/bugfix/fix-tool-names-in-…
Scrattlebeard Feb 2, 2026
a8f0f7c
Merge upstream/main
Reapor-Yurnero Feb 2, 2026
d020951
fix pnpm lint & pnpm build errors
Reapor-Yurnero Feb 2, 2026
43f4fc2
Merge grayswansecurity/openclaw#2
Reapor-Yurnero Feb 2, 2026
6b7c542
add readme for cygnal and gpt-oss-safeguard extensions
Reapor-Yurnero Feb 2, 2026
0e9293b
update doc about restart option
Reapor-Yurnero Feb 2, 2026
3b1e194
fix a typo introduced in command-safety-guard
Reapor-Yurnero Feb 2, 2026
e37120d
Add OpenClaw-specific API link
nwinter Feb 3, 2026
020918e
Update pnpm-lock.yaml for command-safety-guard extension
nwinter Feb 3, 2026
8dc07d2
Format extension files and escape pipe in fork-bomb pattern
nwinter Feb 3, 2026
de84696
Update test to match hook contract requiring toolCallId and messages
nwinter Feb 3, 2026
ed99104
Fix tests: use lowercase tool names and updated DEFAULT_POLICY content
nwinter Feb 3, 2026
aae4098
Skip path tests on Windows (plugin only supports Unix paths)
nwinter Feb 3, 2026
df0ffd9
address greptile comments
Reapor-Yurnero Feb 3, 2026
87e03a9
address false positves in security-audit extension for redirection
Reapor-Yurnero Feb 3, 2026
0b6d963
fix formatting issue in previous commit
Reapor-Yurnero Feb 3, 2026
495f859
address dropped tool return metadata on block
Reapor-Yurnero Feb 3, 2026
d4bea63
address more comments
Reapor-Yurnero Feb 3, 2026
51fcebe
fix type error
Reapor-Yurnero Feb 3, 2026
f1aac5f
Merge upstream/main
Reapor-Yurnero Feb 9, 2026
0a3e77d
Merge upstream/main
Reapor-Yurnero Feb 12, 2026
1141b52
Merge remote-tracking branch 'upstream/main' into feat/guardrail_inte…
Reapor-Yurnero Feb 14, 2026
8c94a4a
Merge remote-tracking branch 'upstream/main' into feat/guardrail_inte…
Reapor-Yurnero Feb 14, 2026
a5c1013
test(memory): stabilize qmd-manager wait
Reapor-Yurnero Feb 14, 2026
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
Prev Previous commit
Next Next commit
Fix implementations to use the correct tool names.
Add missing patterns from PR 6569
Add README.mds
  • Loading branch information
Scrattlebeard committed Feb 2, 2026
commit 87c605144370c4d9b2eaa3085d644ea906660dee
102 changes: 102 additions & 0 deletions extensions/command-safety-guard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Command Safety Guard

Blocks execution of potentially destructive shell commands before they run.

## What this does

This guardrail plugin intercepts `exec` tool calls and checks the command against a set of dangerous patterns. It prevents catastrophic operations like:

- Recursive forced deletion (`rm -rf /`)
- Direct disk writes (`dd of=/dev/sda`)
- Fork bombs and resource exhaustion
- Privilege escalation attempts
- Data exfiltration via network tools

## Built-in rules

| Rule ID | Description | Severity |
|---------|-------------|----------|
| `rm-recursive-force` | `rm -rf` or `rm -fr` flags | error |
| `rm-root` | Deletion targeting `/`, `/bin`, `/usr`, `/etc`, `/var`, `/home`, `/opt`, `/Users`, `~`, `$HOME` | error |
| `rm-current-dir` | Deleting `.`, `..`, or `.*` | error |
| `rm-all-files` | Deleting `*` in current directory | error |
| `find-delete-root` | `find / ... -delete` | error |
| `dd-device` | `dd` writing to `/dev/` | error |
| `mkfs` | Filesystem creation | error |
| `format-disk` | `fdisk`, `parted`, `gdisk` on `/dev/` | error |
| `shutdown-reboot` | `shutdown`, `reboot`, `poweroff`, `halt`, `init 0/6` | error |
| `chmod-recursive-permissive` | `chmod -R 777` | warning |
| `chmod-any-permissive` | Any `chmod 777` | warning |
| `chmod-remove-perms` | `chmod 000` on `/bin`, `/usr`, `/etc` | error |
| `chown-recursive-root` | `chown -R root` on sensitive paths | error |
| `chown-root-dir` | `chown -R` on `/` | error |
| `fork-bomb` | Fork bomb pattern `:(){ :|:& };:` | error |
| `infinite-loop-yes` | `yes \|` infinite output | warning |
| `curl-upload` | Uploading files via `curl -F`, `--form`, `-d`, etc. | warning |
| `nc-listener` | Netcat listener or reverse shell | error |
| `base64-pipe-curl` | Base64 piped to network commands | warning |
| `curl-pipe-interpreter` | `curl/wget ... \| bash/python/etc` | error |
| `cat-ssh-keys` | Reading SSH private keys | error |
| `cat-env-credentials` | Reading `.env`, `.netrc`, `.aws/credentials`, `.npmrc` | error |
| `history-clear` | Clearing shell history | warning |
| `shred-logs` | Shredding log files | error |
| `sudo-passwd` | `sudo passwd` | error |
| `visudo-echo` | `echo ... > /etc/sudoers` | error |
| `system-file-overwrite` | `> /etc/passwd`, `/etc/shadow`, `/etc/group` | error |
| `git-no-verify` | `git commit --no-verify` | error |
| `docker-prune-all` | `docker system prune -a --volumes` | error |

## Configuration

```jsonc
{
"plugins": {
"command-safety-guard": {
// Allow commands through if evaluation fails (default: true)
"failOpen": true,

// Additional regex patterns to block
"extraPatterns": [
"\\bkubectl\\s+delete\\s+namespace"
],

// Patterns to allow even if they match block rules
"allowPatterns": [
"rm -rf ./node_modules"
],

// Disable specific built-in rules
"disabledRules": [
"infinite-loop-yes"
],

// Stage configuration
"stages": {
"beforeToolCall": {
"enabled": true,
"mode": "block" // or "monitor" to log without blocking
}
}
}
}
}
```

## False positive handling

The plugin strips quoted strings before pattern matching to reduce false positives. For example, this command will **not** be blocked:

```bash
echo "To delete everything, run: rm -rf /"
```

Only actual dangerous commands outside of quotes are flagged.

## Severity levels

- **error**: Command is blocked immediately
- **warning**: Command is blocked in `block` mode, logged in `monitor` mode

## Related

- [security-audit](../security-audit/) - Blocks access to sensitive files and credentials
121 changes: 102 additions & 19 deletions extensions/command-safety-guard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,25 @@ const BUILT_IN_RULES: CommandRule[] = [
{
id: "rm-root",
description: "Deletion targeting root or system paths",
pattern: /\brm\s+.*\s+(\/|\/\*|\/bin|\/usr|\/etc|\/var|\/home|\~)\s*$/,
pattern: /\brm\s+.*\s+(\/|\/\*|\/bin|\/usr|\/etc|\/var|\/home|\/opt|\/Users|\~|\$HOME)\s*$/,
severity: "error",
},
{
id: "rm-current-dir",
description: "Deleting current directory or all hidden files",
pattern: /\brm\s+(-[a-zA-Z]*\s+)*(\.|\.\.|\.\*)(\s|$)/,
severity: "error",
},
{
id: "rm-all-files",
description: "Deleting all files in current directory",
pattern: /\brm\s+(-[a-zA-Z]*\s+)*\*(\s|$)/,
severity: "error",
},
{
id: "find-delete-root",
description: "Recursive deletion from root directory",
pattern: /\bfind\s+\/\s+.*-delete/,
severity: "error",
},
{
Expand Down Expand Up @@ -93,12 +111,30 @@ const BUILT_IN_RULES: CommandRule[] = [
pattern: /\bchmod\s+(-R|--recursive)\s+777\b/,
severity: "warning",
},
{
id: "chmod-any-permissive",
description: "chmod 777 is a security risk",
pattern: /\bchmod\s+777\b/,
severity: "warning",
},
{
id: "chmod-remove-perms",
description: "Removing permissions on system directories",
pattern: /\bchmod\s+(-R\s+)?000\s+\/(bin|usr|etc)(\s|$)/,
severity: "error",
},
{
id: "chown-recursive-root",
description: "Recursive chown to root on sensitive paths",
pattern: /\bchown\s+(-R|--recursive)\s+root[:\s].*\s+(\/|\/home|\/etc)\b/,
severity: "error",
},
{
id: "chown-root-dir",
description: "Changing ownership of root directory",
pattern: /\bchown\s+(-R\s+).*\/(\s|$)/,
severity: "error",
},

// Fork bombs and resource exhaustion
{
Expand All @@ -118,7 +154,8 @@ const BUILT_IN_RULES: CommandRule[] = [
{
id: "curl-upload",
description: "Uploading files via curl",
pattern: /\bcurl\b.*(-F|--form|-d|--data|--data-binary|-T|--upload-file)\b.*(@|<)/,
pattern:
/\bcurl\b.*(-F|--form|-d|--data|--data-binary|-T|--upload-file)\b.*(@|<)/,
severity: "warning",
},
{
Expand All @@ -133,27 +170,36 @@ const BUILT_IN_RULES: CommandRule[] = [
pattern: /\bbase64\b.*\|\s*(curl|wget|nc|netcat)/,
severity: "warning",
},
{
id: "curl-pipe-interpreter",
description: "Piping remote content to interpreters",
pattern: /(curl|wget)\s+[^|]*\|\s*(sudo\s+)?(bash|sh|python|ruby|perl)/,
severity: "error",
},

// Credential/key exposure
{
id: "cat-ssh-keys",
description: "Reading SSH private keys",
// Match SSH private keys but exclude .pub files (negative lookahead)
pattern: /\b(cat|head|tail|less|more)\s+[^\n]*(\/\.ssh\/(id_[a-z0-9]+|[a-z0-9_]+_key)(?!\.pub)|\/\.gnupg\/)(\s|$)/,
pattern:
/\b(cat|head|tail|less|more)\s+[^\n]*(\/\.ssh\/(id_[a-z0-9]+|[a-z0-9_]+_key)(?!\.pub)|\/\.gnupg\/)(\s|$)/,
severity: "error",
},
{
id: "cat-env-credentials",
description: "Reading credential files",
pattern: /\b(cat|head|tail|less|more)\s+.*\/(\.env|\.netrc|\.aws\/credentials|\.npmrc)/,
pattern:
/\b(cat|head|tail|less|more)\s+.*\/(\.env|\.netrc|\.aws\/credentials|\.npmrc)/,
severity: "error",
},

// History and audit evasion
{
id: "history-clear",
description: "Clearing shell history",
pattern: /\b(history\s+-c|>\s*~?\/?\.?\.?(bash_|zsh_)?history|unset\s+HISTFILE)\b/,
pattern:
/\b(history\s+-c|>\s*~?\/?\.?\.?(bash_|zsh_)?history|unset\s+HISTFILE)\b/,
severity: "warning",
},
{
Expand All @@ -176,6 +222,28 @@ const BUILT_IN_RULES: CommandRule[] = [
pattern: /\becho\b.*>.*\/etc\/sudoers/,
severity: "error",
},
{
id: "system-file-overwrite",
description: "Overwriting critical system files",
pattern: />\s*\/etc\/(passwd|shadow|group)/,
severity: "error",
},

// Version control safety
{
id: "git-no-verify",
description: "git commit --no-verify bypasses hooks",
pattern: /\bgit\s+commit\b.*--no-verify/,
severity: "error",
},

// Container safety
{
id: "docker-prune-all",
description: "Wiping all Docker data including volumes",
pattern: /docker\s+system\s+prune\s+-a.*--volumes/,
severity: "error",
},
];

// ============================================================================
Expand Down Expand Up @@ -205,6 +273,14 @@ function compilePatterns(patterns: string[]): RegExp[] {
.filter((p): p is RegExp => p !== null);
}

/**
* Remove quoted strings to reduce false positives.
* Commands inside quotes (e.g. echo "rm -rf /") are less likely to be dangerous.
*/
function stripQuotedStrings(cmd: string): string {
return cmd.replace(/'[^']*'/g, "").replace(/"[^"]*"/g, "");
}

// ============================================================================
// Plugin Definition
// ============================================================================
Expand All @@ -224,7 +300,8 @@ const commandSafetyGuardPlugin = createGuardrailPlugin<CommandSafetyConfig>({
return { safe: true };
}

if (ctx.metadata.toolName !== "Bash") {
// Tool name is lowercase "exec" (not "Bash")
if (ctx.metadata.toolName !== "exec") {
return { safe: true };
}

Expand All @@ -233,7 +310,10 @@ const commandSafetyGuardPlugin = createGuardrailPlugin<CommandSafetyConfig>({
return { safe: true };
}

// Check allow patterns first (escape hatch)
// Strip quoted strings to reduce false positives (e.g. echo "rm -rf /")
const cleanedCommand = stripQuotedStrings(command);

// Check allow patterns first (escape hatch) — use original command for allowlist
if (config.allowPatterns && config.allowPatterns.length > 0) {
const allowRegexes = compilePatterns(config.allowPatterns);
if (matchesAnyPattern(command, allowRegexes)) {
Expand All @@ -244,7 +324,7 @@ const commandSafetyGuardPlugin = createGuardrailPlugin<CommandSafetyConfig>({
// Check extra block patterns from config
if (config.extraPatterns && config.extraPatterns.length > 0) {
const extraRegexes = compilePatterns(config.extraPatterns);
if (matchesAnyPattern(command, extraRegexes)) {
if (matchesAnyPattern(cleanedCommand, extraRegexes)) {
return {
safe: false,
reason: "Command matches custom block pattern",
Expand All @@ -259,7 +339,7 @@ const commandSafetyGuardPlugin = createGuardrailPlugin<CommandSafetyConfig>({
if (disabledRules.has(rule.id)) {
continue;
}
if (rule.pattern.test(command)) {
if (rule.pattern.test(cleanedCommand)) {
return {
safe: false,
reason: rule.description,
Expand All @@ -275,16 +355,19 @@ const commandSafetyGuardPlugin = createGuardrailPlugin<CommandSafetyConfig>({
return { safe: true };
},

formatViolationMessage(evaluation: GuardrailEvaluation, location: string): string {
const details = evaluation.details as {
command?: string;
ruleId?: string;
severity?: string;
} | undefined;
formatViolationMessage(
evaluation: GuardrailEvaluation,
location: string,
): string {
const details = evaluation.details as
| {
command?: string;
ruleId?: string;
severity?: string;
}
| undefined;

const parts = [
`Command blocked by safety guard: ${evaluation.reason}.`,
];
const parts = [`Command blocked by safety guard: ${evaluation.reason}.`];

if (details?.ruleId) {
parts.push(`Rule: ${details.ruleId}.`);
Expand All @@ -311,5 +394,5 @@ const pluginWithSchema = {
export default pluginWithSchema;

// Export for testing
export { BUILT_IN_RULES, extractBashCommand };
export { BUILT_IN_RULES, extractBashCommand, stripQuotedStrings };
export type { CommandSafetyConfig, CommandRule };
Loading