-
Notifications
You must be signed in to change notification settings - Fork 278
Add support for intercepting bash #903
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -549,9 +549,34 @@ pi.on("tool_call", async (event, ctx) => { | |
|
|
||
| **Examples:** [chalk-logger.ts](../examples/extensions/chalk-logger.ts), [permission-gate.ts](../examples/extensions/permission-gate.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [protected-paths.ts](../examples/extensions/protected-paths.ts) | ||
|
|
||
| #### before_bash_exec | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would be very useful for direnv integration! I am currently doing this: https://github.com/Mic92/dotfiles/blob/main/home/.pi/agent/extensions/direnv.ts One thing that is missing is knowing what directory the agent tries to access. Often the agent prepends |
||
|
|
||
| Fired before a bash command executes (tool calls and user `!`/`!!`). Use it to rewrite commands or override execution settings. You can also block execution by returning `{ block: true, reason?: string }`. For follow-up hints based on output, pair this with `tool_result`. | ||
|
|
||
| ```typescript | ||
| pi.on("before_bash_exec", async (event) => { | ||
| if (event.command.includes("rm -rf")) { | ||
| return { block: true, reason: "Blocked by policy" }; | ||
| } | ||
|
|
||
| if (event.source === "tool") { | ||
| return { | ||
| cwd: "/tmp", | ||
| env: { | ||
| ...event.env, | ||
| MY_VAR: "1", | ||
| PATH: undefined, // remove PATH | ||
| }, | ||
| }; | ||
| } | ||
| }); | ||
| ``` | ||
|
|
||
| Return a `BashExecOverrides` object to override fields, or return `{ block: true, reason?: string }` to reject the command. Any field set to a non-undefined value replaces the original (`command`, `cwd`, `env`, `shell`, `args`, `timeout`). For `env`, set a key to `undefined` to remove it. | ||
|
|
||
| #### tool_result | ||
|
|
||
| Fired after tool executes. **Can modify result.** | ||
| Fired after tool executes. **Can modify result.** Use this to post-process outputs (for example, append hints or redact secrets) before the result is sent to the model. | ||
|
|
||
| ```typescript | ||
| import { isBashToolResult } from "@mariozechner/pi-coding-agent"; | ||
|
|
@@ -565,10 +590,12 @@ pi.on("tool_result", async (event, ctx) => { | |
| } | ||
|
|
||
| // Modify result: | ||
| return { content: [...], details: {...}, isError: false }; | ||
| return { content: [...], details: {...} }; | ||
| }); | ||
| ``` | ||
|
|
||
| If `event.isError` is true, return `{ content: [...], isError: true }` to override the thrown error message (the text content becomes the error string). Returning `isError: true` on a successful tool result forces the tool to be treated as an error. | ||
|
|
||
| **Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) | ||
|
|
||
| ### User Bash Events | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| /** | ||
| * uv Python Interceptor | ||
| * | ||
| * Demonstrates before_bash_exec by redirecting python invocations through uv. | ||
| * This is a simple example that assumes basic whitespace-separated arguments. | ||
| * | ||
| * Usage: | ||
| * pi -e examples/extensions/uv.ts | ||
| */ | ||
|
|
||
| import { type ExtensionAPI, isBashToolResult } from "@mariozechner/pi-coding-agent"; | ||
|
|
||
| const PYTHON_PREFIX = /^python3?(\s+|$)/; | ||
| const UV_RUN_PYTHON_PREFIX = /^uv\s+run\s+python3?(\s+|$)/; | ||
| const PIP_PREFIX = /^pip3?(\s+|$)/; | ||
| const PIP_MODULE_PATTERN = /\s-m\s+pip3?(\s|$)/; | ||
| const TRACEBACK_PATTERN = /Traceback \(most recent call last\):/; | ||
| const IMPORT_ERROR_PATTERN = /\b(ModuleNotFoundError|ImportError):/; | ||
| const MODULE_NOT_FOUND_PATTERN = /No module named ['"]([^'"]+)['"]/; | ||
|
|
||
| const PIP_BLOCK_REASON = | ||
| "pip is disabled. Use uv run instead, particularly --with and --script for throwaway work. Do not use uv pip!"; | ||
|
|
||
| export default function (pi: ExtensionAPI) { | ||
| pi.on("before_bash_exec", (event) => { | ||
| const trimmed = event.originalCommand.trim(); | ||
| const isPythonCommand = PYTHON_PREFIX.test(trimmed); | ||
| const isUvRunPythonCommand = UV_RUN_PYTHON_PREFIX.test(trimmed); | ||
| const isPipModule = PIP_MODULE_PATTERN.test(trimmed); | ||
|
|
||
| if (PIP_PREFIX.test(trimmed) || (isPipModule && (isPythonCommand || isUvRunPythonCommand))) { | ||
| return { | ||
| block: true, | ||
| reason: PIP_BLOCK_REASON, | ||
| }; | ||
| } | ||
|
|
||
| if (!isPythonCommand) { | ||
| return; | ||
| } | ||
|
|
||
| const normalizedCommand = trimmed.replace(PYTHON_PREFIX, "python ").trimEnd(); | ||
| const uvCommand = `uv run ${normalizedCommand}`; | ||
|
|
||
| return { | ||
| command: uvCommand, | ||
| }; | ||
| }); | ||
|
|
||
| pi.on("tool_result", (event) => { | ||
| if (!isBashToolResult(event)) return; | ||
|
|
||
| const text = event.content | ||
| .filter((item) => item.type === "text") | ||
| .map((item) => item.text) | ||
| .join(""); | ||
|
|
||
| if (!TRACEBACK_PATTERN.test(text) || !IMPORT_ERROR_PATTERN.test(text)) { | ||
| return; | ||
| } | ||
|
|
||
| const moduleMatch = text.match(MODULE_NOT_FOUND_PATTERN); | ||
| const moduleName = moduleMatch?.[1]; | ||
| const hintTarget = moduleName ? ` --with ${moduleName}` : ""; | ||
| const hint = | ||
| "\n\nHint: Python import failed. Use uv to fetch dependencies automatically without changing the system, " + | ||
| `e.g. \`uv run${hintTarget} python -c '...'\` or \`uv run --script\` for throwaway scripts.`; | ||
|
|
||
| return { | ||
| content: [...event.content, { type: "text", text: hint }], | ||
| isError: true, | ||
| }; | ||
| }); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.