feat: add rm -rf block hook script and fix stderr (#661)
This commit is contained in:
20
hooks/block-rm-rf.sh
Executable file
20
hooks/block-rm-rf.sh
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Block dangerous rm -rf commands
|
||||||
|
|
||||||
|
input=$(cat)
|
||||||
|
tool_name=$(echo "$input" | jq -r '.tool_name')
|
||||||
|
|
||||||
|
# Only check Bash commands
|
||||||
|
if [ "$tool_name" != "Bash" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
command=$(echo "$input" | jq -r '.tool_input.command')
|
||||||
|
|
||||||
|
# Check for rm -rf pattern (handles -rf, -fr, -rfi, etc.)
|
||||||
|
if echo "$command" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)'; then
|
||||||
|
echo "Blocked: rm -rf commands must be ran manually, use rm and rmdir instead." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -275,11 +275,12 @@ export async function executeHooks(
|
|||||||
const result = await executeHookCommand(hook, input, workingDirectory);
|
const result = await executeHookCommand(hook, input, workingDirectory);
|
||||||
results.push(result);
|
results.push(result);
|
||||||
|
|
||||||
// Collect feedback from stdout when hook blocks
|
// Collect feedback from stderr when hook blocks
|
||||||
|
// Format: [command]: {stderr} per spec
|
||||||
if (result.exitCode === HookExitCode.BLOCK) {
|
if (result.exitCode === HookExitCode.BLOCK) {
|
||||||
blocked = true;
|
blocked = true;
|
||||||
if (result.stdout) {
|
if (result.stderr) {
|
||||||
feedback.push(result.stdout);
|
feedback.push(`[${hook.command}]: ${result.stderr}`);
|
||||||
}
|
}
|
||||||
// Stop processing more hooks after a block
|
// Stop processing more hooks after a block
|
||||||
break;
|
break;
|
||||||
@@ -320,11 +321,17 @@ export async function executeHooksParallel(
|
|||||||
let blocked = false;
|
let blocked = false;
|
||||||
let errored = false;
|
let errored = false;
|
||||||
|
|
||||||
for (const result of results) {
|
// Zip hooks with results to access command for formatting
|
||||||
|
for (let i = 0; i < results.length; i++) {
|
||||||
|
const result = results[i];
|
||||||
|
const hook = hooks[i];
|
||||||
|
if (!result || !hook) continue;
|
||||||
|
|
||||||
|
// Format: [command]: {stderr} per spec
|
||||||
if (result.exitCode === HookExitCode.BLOCK) {
|
if (result.exitCode === HookExitCode.BLOCK) {
|
||||||
blocked = true;
|
blocked = true;
|
||||||
if (result.stdout) {
|
if (result.stderr) {
|
||||||
feedback.push(result.stdout);
|
feedback.push(`[${hook.command}]: ${result.stderr}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (result.exitCode === HookExitCode.ERROR) {
|
if (result.exitCode === HookExitCode.ERROR) {
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ describe.skipIf(isWindows)("Hooks Executor", () => {
|
|||||||
test("stops on first blocking hook", async () => {
|
test("stops on first blocking hook", async () => {
|
||||||
const hooks: HookCommand[] = [
|
const hooks: HookCommand[] = [
|
||||||
{ type: "command", command: "echo 'allowed'" },
|
{ type: "command", command: "echo 'allowed'" },
|
||||||
{ type: "command", command: "echo 'blocked' && exit 2" },
|
{ type: "command", command: "echo 'blocked' >&2 && exit 2" },
|
||||||
{ type: "command", command: "echo 'should not run'" },
|
{ type: "command", command: "echo 'should not run'" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -203,7 +203,7 @@ describe.skipIf(isWindows)("Hooks Executor", () => {
|
|||||||
|
|
||||||
expect(result.blocked).toBe(true);
|
expect(result.blocked).toBe(true);
|
||||||
expect(result.results).toHaveLength(2); // Only first two ran
|
expect(result.results).toHaveLength(2); // Only first two ran
|
||||||
expect(result.feedback).toContain("blocked");
|
expect(result.feedback[0]).toContain("blocked");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("continues after error but tracks it", async () => {
|
test("continues after error but tracks it", async () => {
|
||||||
@@ -247,7 +247,7 @@ describe.skipIf(isWindows)("Hooks Executor", () => {
|
|||||||
const hooks: HookCommand[] = [
|
const hooks: HookCommand[] = [
|
||||||
{
|
{
|
||||||
type: "command",
|
type: "command",
|
||||||
command: "echo 'Reason: file is dangerous' && exit 2",
|
command: "echo 'Reason: file is dangerous' >&2 && exit 2",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -261,7 +261,7 @@ describe.skipIf(isWindows)("Hooks Executor", () => {
|
|||||||
const result = await executeHooks(hooks, input, tempDir);
|
const result = await executeHooks(hooks, input, tempDir);
|
||||||
|
|
||||||
expect(result.blocked).toBe(true);
|
expect(result.blocked).toBe(true);
|
||||||
expect(result.feedback).toContain("Reason: file is dangerous");
|
expect(result.feedback[0]).toContain("Reason: file is dangerous");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("collects error feedback from stderr", async () => {
|
test("collects error feedback from stderr", async () => {
|
||||||
|
|||||||
@@ -110,7 +110,8 @@ describe.skipIf(isWindows)("Hooks Integration Tests", () => {
|
|||||||
hooks: [
|
hooks: [
|
||||||
{
|
{
|
||||||
type: "command",
|
type: "command",
|
||||||
command: "echo 'Blocked: write to sensitive file' && exit 2",
|
command:
|
||||||
|
"echo 'Blocked: write to sensitive file' >&2 && exit 2",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -125,7 +126,7 @@ describe.skipIf(isWindows)("Hooks Integration Tests", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(result.blocked).toBe(true);
|
expect(result.blocked).toBe(true);
|
||||||
expect(result.feedback).toContain("Blocked: write to sensitive file");
|
expect(result.feedback[0]).toContain("Blocked: write to sensitive file");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("matches by tool name pattern", async () => {
|
test("matches by tool name pattern", async () => {
|
||||||
@@ -277,7 +278,7 @@ describe.skipIf(isWindows)("Hooks Integration Tests", () => {
|
|||||||
hooks: [
|
hooks: [
|
||||||
{
|
{
|
||||||
type: "command",
|
type: "command",
|
||||||
command: "echo 'Denied: dangerous command' && exit 2",
|
command: "echo 'Denied: dangerous command' >&2 && exit 2",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -293,7 +294,7 @@ describe.skipIf(isWindows)("Hooks Integration Tests", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(result.blocked).toBe(true);
|
expect(result.blocked).toBe(true);
|
||||||
expect(result.feedback).toContain("Denied: dangerous command");
|
expect(result.feedback[0]).toContain("Denied: dangerous command");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("receives permission type and scope in input", async () => {
|
test("receives permission type and scope in input", async () => {
|
||||||
@@ -599,7 +600,7 @@ describe.skipIf(isWindows)("Hooks Integration Tests", () => {
|
|||||||
hooks: [
|
hooks: [
|
||||||
{
|
{
|
||||||
type: "command",
|
type: "command",
|
||||||
command: "echo 'Cannot compact now' && exit 2",
|
command: "echo 'Cannot compact now' >&2 && exit 2",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -615,7 +616,7 @@ describe.skipIf(isWindows)("Hooks Integration Tests", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(result.blocked).toBe(true);
|
expect(result.blocked).toBe(true);
|
||||||
expect(result.feedback).toContain("Cannot compact now");
|
expect(result.feedback[0]).toContain("Cannot compact now");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("receives context info in input", async () => {
|
test("receives context info in input", async () => {
|
||||||
@@ -887,7 +888,7 @@ describe.skipIf(isWindows)("Hooks Integration Tests", () => {
|
|||||||
matcher: "*",
|
matcher: "*",
|
||||||
hooks: [
|
hooks: [
|
||||||
{ type: "command", command: "echo 'check 1'" },
|
{ type: "command", command: "echo 'check 1'" },
|
||||||
{ type: "command", command: "echo 'BLOCKED' && exit 2" },
|
{ type: "command", command: "echo 'BLOCKED' >&2 && exit 2" },
|
||||||
{ type: "command", command: "echo 'should not run'" },
|
{ type: "command", command: "echo 'should not run'" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -898,7 +899,7 @@ describe.skipIf(isWindows)("Hooks Integration Tests", () => {
|
|||||||
|
|
||||||
expect(result.blocked).toBe(true);
|
expect(result.blocked).toBe(true);
|
||||||
expect(result.results).toHaveLength(2);
|
expect(result.results).toHaveLength(2);
|
||||||
expect(result.feedback).toContain("BLOCKED");
|
expect(result.feedback[0]).toContain("BLOCKED");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("error hooks do not block subsequent hooks", async () => {
|
test("error hooks do not block subsequent hooks", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user