disable_startup_item

disable_startup_item

Removes an application from the login items list so it no longer launches at startup. Use to reduce boot time or stop unwanted background apps. Does not uninstall the application.

Metadata

Name

disable_startup_item

Updated

3 weeks ago

Source

GitHub source

Risk

Medium

Requires consent

true

Affected scope

user

Code

/**
 * mcp/skills/disableStartupItem.ts — disable_startup_item skill
 *
 * Removes an application from the login items list so it no longer launches
 * at startup. Does not uninstall the application. Defaults to dry-run.
 *
 * Platform strategy
 * -----------------
 * darwin  osascript to delete login item; fallback: remove plist from LaunchAgents
 * win32   PowerShell remove from HKCU Run registry key
 *
 * Smoke test
 *   npx tsx -r dotenv/config mcp/skills/disableStartupItem.ts
 */

import * as os       from "os";
import * as nodePath from "path";
import { exec }      from "child_process";
import { promisify } from "util";
import { z }         from "zod";
import * as fs       from "fs/promises";

const execAsync = promisify(exec);

// -- Meta ---------------------------------------------------------------------

export const meta = {
  name: "disable_startup_item",
  description:
    "Removes an application from the login items list so it no longer launches " +
    "at startup. Use to reduce boot time or stop unwanted background apps. " +
    "Does not uninstall the application.",
  riskLevel:       "medium",
  destructive:     true,
  requiresConsent: true,
  supportsDryRun:  true,
  affectedScope:   ["user"],
  auditRequired:   true,
  schema: {
    names: z
      .array(z.string().min(1))
      .min(1)
      .describe("Exact names of the startup items to disable, in one batch"),
    dryRun: z
      .boolean()
      .optional()
      .describe("If true, show what would be removed without removing. Default: true"),
  },
} as const;

// -- Types --------------------------------------------------------------------

interface DisableResult {
  name:     string;
  found:    boolean;
  disabled: boolean;
  dryRun:   boolean;
  message:  string;
}

interface DisableStartupItemResult {
  platform:     NodeJS.Platform;
  dryRun:       boolean;
  results:      DisableResult[];
  affectedCount: number;
  message:      string;
}

// -- 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 disableStartupItemDarwin(
  name:   string,
  dryRun: boolean,
): Promise<DisableResult> {
  const safeName = name.replace(/'/g, `'\''`);

// 1. Try osascript login items first
  let found = false;
  try {
    const { stdout } = await execAsync(
      `osascript -e 'tell application "System Events" to get the name of every login item' 2>/dev/null`,
      { maxBuffer: 1024 * 1024 },
    );
    const names = stdout.trim().split(", ").map(s => s.trim());
    found = names.some(n => n.toLowerCase() === name.toLowerCase());
  } catch {
    // osascript unavailable
  }

if (found) {
    if (dryRun) {
      return { name, found: true, disabled: false, dryRun: true, message: `Dry run: would remove login item '${name}'.` };
    }
    try {
      await execAsync(
        `osascript -e 'tell application "System Events" to delete login item "${safeName}"' 2>/dev/null`,
        { maxBuffer: 1024 * 1024 },
      );
      return { name, found: true, disabled: true, dryRun: false, message: `Login item '${name}' removed.` };
    } catch (err) {
      const msg = (err as Error).message ?? String(err);
      return { name, found: true, disabled: false, dryRun: false, message: `Failed to remove login item: ${msg}` };
    }
  }

// 2. Fallback: search LaunchAgents plist
  const home       = os.homedir();
  const agentDir   = nodePath.join(home, "Library", "LaunchAgents");
  const plistName  = `${name}.plist`;
  const plistPath  = nodePath.join(agentDir, plistName);

let plistExists = false;
  try {
    await fs.access(plistPath);
    plistExists = true;
  } catch {
    // not found
  }

if (!plistExists) {
    return { name, found: false, disabled: false, dryRun, message: `No login item or LaunchAgent named '${name}' found.` };
  }

if (dryRun) {
    return { name, found: true, disabled: false, dryRun: true, message: `Dry run: would remove LaunchAgent plist '${plistPath}'.` };
  }

try {
    await fs.unlink(plistPath);
    return { name, found: true, disabled: true, dryRun: false, message: `LaunchAgent plist '${plistPath}' removed.` };
  } catch (err) {
    const msg = (err as Error).message ?? String(err);
    return { name, found: true, disabled: false, dryRun: false, message: `Failed to remove plist: ${msg}` };
  }
}

// -- win32 implementation -----------------------------------------------------

async function disableStartupItemWin32(
  name:   string,
  dryRun: boolean,
): Promise<DisableResult> {
  const safeName = name.replace(/'/g, "'');
  const checkPs  = `
$key = Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' -ErrorAction SilentlyContinue
if ($key -and $key.PSObject.Properties['${safeName}']) { 'found' } else { 'notfound' }`.trim();

const checkResult = await runPS(checkPs);
  const found       = checkResult.trim() === "found";

if (!found) {
    return { name, found: false, disabled: false, dryRun, message: `No Run key entry named '${name}' found in HKCU.` };
  }

if (dryRun) {
    return { name, found: true, disabled: false, dryRun: true, message: `Dry run: would remove HKCU Run entry '${name}'.` };
  }

try {
    await runPS(`Remove-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' -Name '${safeName}' -ErrorAction Stop`);
    return { name, found: true, disabled: true, dryRun: false, message: `Run key entry '${name}' removed from HKCU.` };
  } catch (err) {
    const msg = (err as Error).message ?? String(err);
    return { name, found: true, disabled: false, dryRun: false, message: `Failed to remove registry entry: ${msg}` };
  }
}

// -- Exported run function ----------------------------------------------------

export async function run({
  names,
  dryRun = true,
}: {
  names:   string[];
  dryRun?: boolean;
}): Promise<DisableStartupItemResult> {
  const list = (names ?? []).map(n => n?.trim()).filter(Boolean);
  if (list.length === 0) {
    throw new Error("[disable_startup_item] 'names' is required and must contain at least one non-empty name.");
  }

const platform = os.platform();
  const disableOne = platform === "win32" ? disableStartupItemWin32 : disableStartupItemDarwin;

const results: DisableResult[] = [];
  for (const name of list) {
    results.push(await disableOne(name, dryRun));
  }

const affectedCount = dryRun
    ? results.filter(r => r.found).length
    : results.filter(r => r.disabled).length;
  const names_ = results.filter(r => (dryRun ? r.found : r.disabled)).map(r => r.name);
  const message = dryRun
    ? (affectedCount > 0
        ? `Dry run: would remove ${affectedCount} startup item(s): ${names_.join(", ")}.`
        : "Dry run: none of the named startup items were found.")
    : (affectedCount > 0
        ? `Removed ${affectedCount} of ${list.length} startup item(s): ${names_.join(", ")}.`
        : "No startup items were removed (none found, or removal failed — see results).");

return { platform, dryRun, results, affectedCount, message };
}

// -- Smoke test ---------------------------------------------------------------

if (false) {
  run({ names: ["SomeApp"] })
    .then(r => console.log(JSON.stringify(r, null, 2)))
    .catch((err: Error) => { console.error(err.message); process.exit(1); });
}