Skip to content

Commit 8db6f28

Browse files
committed
feat(readme): ✨ Allow virtual environment python commands
Updated the Command Blocker Plugin to permit the execution of Python commands from virtual environments. This change enhances usability for developers working with virtual environments while maintaining the integrity of command blocking for other contexts. - Added exceptions for `.venv/bin/python`, `venv/bin/python`, and `env/bin/python` commands. - Updated tests to ensure virtual environment commands are allowed. This improves the development experience without compromising the plugin's primary functionality.
1 parent 0e15118 commit 8db6f28

File tree

3 files changed

+83
-34
lines changed

3 files changed

+83
-34
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ The plugin blocks various commands to promote better development practices:
1717

1818
- **`pip`** - Blocked in favor of `uv` or `uvx`
1919
- **`python`**, **`python2`**, **`python3`** - Blocked in favor of `uv` or `uvx`
20+
- **Exception**: Virtual environment python commands are allowed:
21+
-`.venv/bin/python`, `.venv/bin/python3`
22+
-`venv/bin/python`, `venv/bin/python3`
23+
-`env/bin/python`, `env/bin/python3`
2024

2125
#### Git Commands
2226

@@ -73,6 +77,10 @@ bunx create-react-app my-app
7377
uv sync
7478
uvx ruff check .
7579

80+
# Virtual environment python (allowed)
81+
.venv/bin/python script.py
82+
venv/bin/python3 -c "print('hello')"
83+
7684
# Git read operations
7785
git status
7886
git diff HEAD~1
@@ -90,7 +98,7 @@ nix run github:nix-community/nixpkgs-fmt#nixpkgs-fmt
9098
node --version
9199
npm install
92100
pip install requests
93-
python script.py
101+
python script.py # (but .venv/bin/python is allowed)
94102
git add .
95103
nix run ./my-flake#hello
96104
```

command-blocker.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,44 @@ describe("Command Blocker", () => {
8282
);
8383
});
8484

85+
it("should allow virtual environment python commands", async () => {
86+
const input1 = { tool: "bash" };
87+
const output1 = { args: { command: ".venv/bin/python script.py" } };
88+
await expect(
89+
plugin["tool.execute.before"](input1, output1)
90+
).resolves.toBeUndefined();
91+
92+
const input2 = { tool: "bash" };
93+
const output2 = { args: { command: ".venv/bin/python3 -c 'print(\"hello\")'" } };
94+
await expect(
95+
plugin["tool.execute.before"](input2, output2)
96+
).resolves.toBeUndefined();
97+
98+
const input3 = { tool: "bash" };
99+
const output3 = { args: { command: "venv/bin/python manage.py runserver" } };
100+
await expect(
101+
plugin["tool.execute.before"](input3, output3)
102+
).resolves.toBeUndefined();
103+
104+
const input4 = { tool: "bash" };
105+
const output4 = { args: { command: "env/bin/python3 -c 'print(\"hello\")'" } };
106+
await expect(
107+
plugin["tool.execute.before"](input4, output4)
108+
).resolves.toBeUndefined();
109+
110+
const input5 = { tool: "bash" };
111+
const output5 = { args: { command: "./.venv/bin/python test.py" } };
112+
await expect(
113+
plugin["tool.execute.before"](input5, output5)
114+
).resolves.toBeUndefined();
115+
116+
const input6 = { tool: "bash" };
117+
const output6 = { args: { command: "../venv/bin/python3 script.py" } };
118+
await expect(
119+
plugin["tool.execute.before"](input6, output6)
120+
).resolves.toBeUndefined();
121+
});
122+
85123
it("should allow which/whereis commands", async () => {
86124
const input1 = { tool: "bash" };
87125
const output1 = { args: { command: "which node" } };

command-blocker.ts

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ function checkPythonNodeCommand(command: string): void {
6666
const isWhichOrWhereis: boolean =
6767
command.includes("which") || command.includes("whereis");
6868

69+
// Allow python commands from virtual environments
70+
const venvPatterns: RegExp[] = [
71+
/^[./\\]*\.venv[/\\]bin[/\\]python\d*$/, // .venv/bin/python, .venv/bin/python3
72+
/^[./\\]*venv[/\\]bin[/\\]python\d*$/, // venv/bin/python, venv/bin/python3
73+
/^[./\\]*env[/\\]bin[/\\]python\d*$/, // env/bin/python, env/bin/python3
74+
];
75+
6976
for (const blockedCmd of BLOCKED_COMMANDS) {
7077
if (blockedCmd === "git" || blockedCmd === "nix") continue;
7178

@@ -109,29 +116,35 @@ function checkPythonNodeCommand(command: string): void {
109116

110117
// Enhanced pattern matching for complex command structures
111118
if (!isWhichOrWhereis) {
112-
// Check for blocked commands in various contexts
113-
const patterns: RegExp[] = [
114-
// Direct command anywhere
115-
new RegExp(`\\b${blockedCmd}\\b`, "g"),
116-
// In command substitution $(...)
117-
new RegExp(`\\$\\([^)]*\\b${blockedCmd}\\b[^)]*\\)`, "g"),
118-
// In backticks `...`
119-
new RegExp(`\`[^\`]*\\b${blockedCmd}\\b[^\`]*\``, "g"),
120-
// In quoted strings within commands
121-
new RegExp(`["'][^"']*\\b${blockedCmd}\\b[^"']*["']`, "g"),
122-
// After operators like &&, ||, ;, |
123-
new RegExp(`[;&|]{1,2}\\s*\\b${blockedCmd}\\b`, "g"),
124-
// In background execution &
125-
new RegExp(`\\b${blockedCmd}\\b\\s*&`, "g"),
126-
// With redirection
127-
new RegExp(`\\b${blockedCmd}\\b\\s*[<>]`, "g"),
128-
// Escaped characters
129-
new RegExp(`\\b${blockedCmd.replace(/(.)/g, "$1\\\\?")}\\b`, "g"),
130-
];
131-
132-
for (const pattern of patterns) {
133-
if (pattern.test(command)) {
134-
throw new Error(BLOCKED_COMMAND_MESSAGES[blockedCmd]);
119+
// Check for blocked commands in various contexts, but skip if it's a virtual environment python
120+
const isVenvPython =
121+
blockedCmd.startsWith("python") &&
122+
venvPatterns.some((pattern) => pattern.test(actualFirstCommand));
123+
124+
if (!isVenvPython) {
125+
const patterns: RegExp[] = [
126+
// Direct command anywhere
127+
new RegExp(`\\b${blockedCmd}\\b`, "g"),
128+
// In command substitution $(...)
129+
new RegExp(`\\$\\([^)]*\\b${blockedCmd}\\b[^)]*\\)`, "g"),
130+
// In backticks `...`
131+
new RegExp(`\`[^\`]*\\b${blockedCmd}\\b[^\`]*\``, "g"),
132+
// In quoted strings within commands
133+
new RegExp(`["'][^"']*\\b${blockedCmd}\\b[^"']*["']`, "g"),
134+
// After operators like &&, ||, ;, |
135+
new RegExp(`[;&|]{1,2}\\s*\\b${blockedCmd}\\b`, "g"),
136+
// In background execution &
137+
new RegExp(`\\b${blockedCmd}\\b\\s*&`, "g"),
138+
// With redirection
139+
new RegExp(`\\b${blockedCmd}\\b\\s*[<>]`, "g"),
140+
// Escaped characters
141+
new RegExp(`\\b${blockedCmd.replace(/(.)/g, "$1\\\\?")}\\b`, "g"),
142+
];
143+
144+
for (const pattern of patterns) {
145+
if (pattern.test(command)) {
146+
throw new Error(BLOCKED_COMMAND_MESSAGES[blockedCmd]);
147+
}
135148
}
136149
}
137150
}
@@ -309,12 +322,6 @@ function checkReadOnlyFileEdit(filePath: string): void {
309322
}
310323
}
311324

312-
313-
314-
315-
316-
317-
318325
export const CommandBlocker: Plugin = async ({
319326
app,
320327
client,
@@ -328,12 +335,8 @@ export const CommandBlocker: Plugin = async ({
328335

329336
if (input.tool === "edit") {
330337
const newString = output.args.newString || "";
331-
332-
333338
} else if (input.tool === "write") {
334339
const content = output.args.content || "";
335-
336-
337340
}
338341
}
339342

0 commit comments

Comments
 (0)