kill_process
kill_process
Terminates a process by name or PID. Use when a process is unresponsive or consuming excessive resources. Always confirm with user before killing critical system processes.
Metadata
Name
kill_process
Updated
3 weeks ago
Source
Risk
Medium
Requires consent
true
Affected scope
user
Code
/**
* mcp/skills/killProcess.ts — kill_process skill
*
* Terminates a process by name or PID. Defaults to dry-run to prevent
* accidental termination of critical system processes.
*
* Platform strategy
* -----------------
* darwin pkill / kill builtins; pgrep for dry-run matching
* win32 PowerShell Get-Process | Stop-Process
*
* Smoke test
* npx tsx -r dotenv/config mcp/skills/killProcess.ts
*/
import * as os from "os";
import { exec } from "child_process";
import { promisify } from "util";
import { z } from "zod";
import { isAgentSelf, isSystemProcess } from "./_shared/processIdentity";
const execAsync = promisify(exec);
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "kill_process",
description:
"Terminates a process by name or PID. Use when a process is unresponsive " +
"or consuming excessive resources. Always confirm with user before killing " +
"critical system processes.",
riskLevel: "medium",
destructive: true,
requiresConsent: true,
supportsDryRun: true,
affectedScope: ["user"],
auditRequired: true,
schema: {
name: z
.string()
.optional()
.describe("Process name to kill (case-insensitive partial match)"),
pid: z
.number()
.optional()
.describe("Exact PID to kill. More precise than name"),
signal: z
.enum(["TERM", "KILL"])
.optional()
.describe("Signal to send. TERM=graceful shutdown, KILL=force. Default: TERM"),
dryRun: z
.boolean()
.optional()
.describe("If true, show what would be killed without killing. Default: true"),
},
} as const;
// -- Types --------------------------------------------------------------------
interface MatchedProcess {
pid: number;
name: string;
}
interface KillResult {
matched: MatchedProcess[];
killed: boolean;
dryRun: boolean;
signal: string;
message: string;
}
// -- Guards -------------------------------------------------------------------
/**
* Returns a human-readable reason when a process must NOT be killed, else null.
* Covers: the agent itself (never terminate the running agent), PID 1, and
* critical OS processes (shared SYSTEM_PROCESS_NAMES / system paths).
*/
function protectionReason(pid: number | undefined, name: string | undefined): string | null {
if (isAgentSelf(name, pid)) return "the AI Support Agent itself";
if (pid === 1) return "PID 1 (init)";
if (isSystemProcess(name)) return "a protected system process";
return null;
}
function isProtected(pid: number | undefined, name: string | undefined): boolean {
return protectionReason(pid, name) !== null;
}
// -- PowerShell helper --------------------------------------------------------
async function runPS(script: string): Promise<string> {
const encoded = Buffer.from(script, "utf16le").toString("base64");
const { stdout } = await execAsync(
`powershell.exe -NoProfile -NonInteractive -EncodedCommand ${encoded}`,
{ maxBuffer: 10 * 1024 * 1024 },
);
return stdout.trim();
}
// -- darwin implementation ----------------------------------------------------
async function killProcessDarwin(
name: string | undefined,
pid: number | undefined,
signal: string,
dryRun: boolean,
): Promise<KillResult> {
const matched: MatchedProcess[] = [];
if (pid !== undefined) {
// Resolve process name for the given PID
try {
const { stdout } = await execAsync(
`ps -p ${pid} -o comm= 2>/dev/null`,
{ maxBuffer: 1024 * 1024 },
);
const procName = stdout.trim().split("/").at(-1) ?? "";
if (procName) matched.push({ pid, name: procName });
} catch {
// process may not exist
}
} else if (name) {
const safeName = name.replace(/'/g, `'\\''`);
try {
const { stdout } = await execAsync(
`pgrep -i -l '${safeName}' 2>/dev/null`,
{ maxBuffer: 1024 * 1024 },
);
for (const line of stdout.trim().split("\n").filter(Boolean)) {
const parts = line.trim().split(/\s+/);
const p = parseInt(parts[0], 10);
const n = parts.slice(1).join(" ").split("/").at(-1) ?? parts[1] ?? "";
if (!isNaN(p)) matched.push({ pid: p, name: n });
}
} catch {
// no matches
}
}
if (matched.length === 0) {
return { matched, killed: false, dryRun, signal, message: "No matching processes found." };
}
// Check for protected processes
for (const m of matched) {
if (isProtected(m.pid, m.name)) {
return {
matched,
killed: false,
dryRun,
signal,
message: `Refused: process '${m.name}' (PID ${m.pid}) is ${protectionReason(m.pid, m.name) ?? "a protected system process"}.`,
};
}
}
if (dryRun) {
return {
matched,
killed: false,
dryRun: true,
signal,
message: `Dry run: would send SIG${signal} to ${matched.length} process(es).`,
};
}
try {
if (pid !== undefined) {
await execAsync(`kill -${signal} ${pid} 2>/dev/null`);
} else if (name) {
const safeName = name.replace(/'/g, `'\\''`);
await execAsync(`pkill -${signal} -i '${safeName}' 2>/dev/null`);
}
return {
matched,
killed: true,
dryRun: false,
signal,
message: `Sent SIG${signal} to ${matched.length} process(es).`,
};
} catch (err) {
const msg = (err as Error).message ?? String(err);
return { matched, killed: false, dryRun: false, signal, message: `Kill failed: ${msg}` };
}
}
// -- win32 implementation -----------------------------------------------------
async function killProcessWin32(
name: string | undefined,
pid: number | undefined,
signal: string,
dryRun: boolean,
): Promise<KillResult> {
const force = signal === "KILL" ? "-Force" : "";
let listPs: string;
if (pid !== undefined) {
listPs = `Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object Id,ProcessName | ConvertTo-Json -Depth 1 -Compress`;
} else {
const safeName = (name ?? "").replace(/'/g, "''");
listPs = `Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessName -like '*${safeName}*' } | Select-Object Id,ProcessName | ConvertTo-Json -Depth 1 -Compress`;
}
const raw = await runPS(listPs);
if (!raw) {
return { matched: [], killed: false, dryRun, signal, message: "No matching processes found." };
}
let arr: { Id: number; ProcessName: string }[];
try {
const parsed = JSON.parse(raw) as { Id: number; ProcessName: string } | { Id: number; ProcessName: string }[];
arr = Array.isArray(parsed) ? parsed : [parsed];
} catch {
return { matched: [], killed: false, dryRun, signal, message: "No matching processes found." };
}
const matched: MatchedProcess[] = arr.map(p => ({ pid: p.Id, name: p.ProcessName }));
for (const m of matched) {
if (isProtected(m.pid, m.name)) {
return {
matched,
killed: false,
dryRun,
signal,
message: `Refused: process '${m.name}' (PID ${m.pid}) is ${protectionReason(m.pid, m.name) ?? "a protected system process"}.`,
};
}
}
if (dryRun) {
return {
matched,
killed: false,
dryRun: true,
signal,
message: `Dry run: would stop ${matched.length} process(es).`,
};
}
let stopPs: string;
if (pid !== undefined) {
stopPs = `Stop-Process -Id ${pid} ${force} -ErrorAction SilentlyContinue`;
} else {
const safeName = (name ?? "").replace(/'/g, "''");
stopPs = `Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessName -like '*${safeName}*' } | Stop-Process ${force} -ErrorAction SilentlyContinue`;
}
try {
await runPS(stopPs);
return { matched, killed: true, dryRun: false, signal, message: `Stopped ${matched.length} process(es).` };
} catch (err) {
const msg = (err as Error).message ?? String(err);
return { matched, killed: false, dryRun: false, signal, message: `Stop failed: ${msg}` };
}
}
// -- Exported run function ----------------------------------------------------
export async function run({
name,
pid,
signal = "TERM",
dryRun = true,
}: {
name?: string;
pid?: number;
signal?: "TERM" | "KILL";
dryRun?: boolean;
} = {}): Promise<KillResult> {
if (!name && pid === undefined) {
throw new Error("[kill_process] Must provide either 'name' or 'pid'.");
}
const platform = os.platform();
if (platform === "win32") {
return killProcessWin32(name, pid, signal, dryRun);
}
return killProcessDarwin(name, pid, signal, dryRun);
}
// -- Smoke test ---------------------------------------------------------------
if (false) {
run({})
.then(r => console.log(JSON.stringify(r, null, 2)))
.catch((err: Error) => { console.error(err.message); process.exit(1); });
}