ext-toggle: refuse to disable ssh-controlmaster during --ssh session

Disabling ssh-controlmaster mid --ssh session would tear down the
ControlMaster (if we own it) and silently redirect read/write/edit/bash
back to the local filesystem while the system prompt still claims we're
on the remote. Now blocked with an explanatory dialog.

Implementation: a DISABLE_GUARDS map keyed by bare extension name lets
specific extensions register a refusal predicate. ssh-controlmaster's
guard checks process.argv for --ssh and refuses if present. Easy to
extend with similar foot-guns later.
This commit is contained in:
2026-05-07 20:43:20 +02:00
parent 9f38ba7797
commit c624eafe64
3 changed files with 49 additions and 0 deletions
+40
View File
@@ -21,6 +21,36 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
const EXT_DIR = path.join(os.homedir(), ".pi", "agent", "extensions");
/**
* Per-extension guards that refuse a disable when toggling would silently
* break in-flight session state. Returns a string explaining why the
* disable should be blocked, or null to allow it.
*
* Keyed by bare extension name (matches the file basename without `.ts`).
*/
const DISABLE_GUARDS: Record<string, () => string | null> = {
"ssh-controlmaster": () => {
// If pi was launched with --ssh, disabling will (a) tear down the
// ControlMaster (if we own it) and (b) silently revert read/write/
// edit/bash to the LOCAL filesystem while the system prompt still
// claims we're on the remote. Refuse, point at the safe path.
const hasSshFlag = process.argv.some(
(a) => a === "--ssh" || a.startsWith("--ssh="),
);
if (!hasSshFlag) return null;
return [
"Refusing to disable ssh-controlmaster during an active --ssh session.",
"",
"Disabling now would:",
" \u2022 tear down the SSH ControlMaster (if this extension started it)",
" \u2022 silently redirect read/write/edit/bash back to the LOCAL",
" machine while the system prompt still says you're on the remote",
"",
"Exit pi and relaunch without --ssh instead.",
].join("\n");
},
};
type Entry = {
name: string; // bare name without extension (e.g. "notify")
fullPath: string; // absolute path on disk
@@ -142,6 +172,16 @@ export default function extToggleExtension(pi: ExtensionAPI) {
return;
}
// Run guard before prompting — only blocks disables, not re-enables.
if (entry.enabled) {
const guard = DISABLE_GUARDS[entry.name];
const reason = guard?.();
if (reason) {
await ctx.ui.confirm(`Cannot disable "${entry.name}"`, reason);
return;
}
}
const verb = entry.enabled ? "Disable" : "Enable";
const ok = await ctx.ui.confirm(
`${verb} "${entry.name}"?`,