VYPR
Medium severity6.3NVD Advisory· Published Feb 8, 2026· Updated Apr 29, 2026

CVE-2026-2178

CVE-2026-2178

Description

A vulnerability was found in r-huijts xcode-mcp-server up to f3419f00117aa9949e326f78cc940166c88f18cb. This affects the function registerXcodeTools of the file src/tools/xcode/index.ts of the component run_lldb. The manipulation of the argument args results in command injection. It is possible to launch the attack remotely. The exploit has been made public and could be used. This product takes the approach of rolling releases to provide continious delivery. Therefore, version details for affected and updated releases are not available. The patch is identified as 11f8d6bacadd153beee649f92a78a9dad761f56f. Applying a patch is advised to resolve this issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
xcode-mcp-servernpm
<= 1.0.3

Affected products

1

Patches

1
11f8d6bacadd

Refactor subprocess invocations to use runExecFile for improved security

11 files changed · +478 844
  • docs/exec-safety-audit.md+34 15 modified
    @@ -1,16 +1,35 @@
    -# execAsync Injection Review
    -
    -This document summarizes each usage of `execAsync` within `src/tools` and whether
    -user supplied input is passed to the shell. When a potential injection vector is
    -found, a mitigation strategy is noted.
    -
    -| File & Line | Parameters | Status |
    -|-------------|-----------|--------|
    -| `project/index.ts` lines around 337 | `cleanedPath` from user path | Switched to `execFile` to avoid shell injection |
    -| `simulator/index.ts` boot/install/etc | `udid`, `bundleId`, `url` parameters | Potential injection; consider validating against `[0-9A-F-]+` and using `execFile` |
    -| `file/index.ts` copy/info operations | validated file paths | Quoting with `"` may allow injection if path contains quotes. Use `execFile` or sanitize path. |
    -| `build/index.ts` build/clean/archive commands | `scheme`, `configuration`, paths | Strings passed directly to shell; sanitize or use `execFile`. |
    -| `spm/index.ts` package commands | package names, options | Strings interpolated; validate or use `execFile`. |
    -| `cocoapods/index.ts` | boolean flags only | Low risk. |
    -| `xcode/index.ts` various commands | parameters like `command`, `developerDir` | Validate or use `execFile`. |
    +# execAsync Injection Review (CWE-78 Remediation)
     
    +## Policy
    +
    +**All subprocess invocations must use `runExecFile` (from `src/utils/execFile.js`) with an argument array.** Do not use `child_process.exec()` or interpolate user-controlled or path-derived strings into a shell command string. This prevents CWE-78 OS Command Injection.
    +
    +- **Safe:** `runExecFile("xcrun", ["lldb", "-o", userCommand, "-b"])` — arguments are passed literally to the process; no shell is invoked.
    +- **Unsafe:** `exec(\`xcrun lldb ${userArgs}\`)` — a shell parses `userArgs`, so e.g. `& ping -c 2 evil.com &` runs arbitrary commands.
    +
    +Use `options.cwd` for working directory instead of `cd "${dir}" && ...`.
    +
    +## Remediation Status (2026)
    +
    +All previously identified injection vectors have been addressed:
    +
    +| Module | Change |
    +|--------|--------|
    +| **xcode/index.ts** | `run_xcrun`, `run_lldb`, `validate_app`, `compile_asset_catalog`, `trace_app`, `switch_xcode`, `export_archive`, `generate_icon_set`, and internal helpers now use `runExecFile` with argument arrays. Credentials and user args are never passed through a shell. |
    +| **build/index.ts** | All `xcodebuild` and `swift build` invocations use `runExecFile("xcodebuild", args, { cwd })` or `runExecFile("swift", args, { cwd })` with args built as arrays. |
    +| **simulator/index.ts** | All `xcrun simctl` calls use `runExecFile("xcrun", ["simctl", subcommand, ...args])`. UDID format is validated with a strict regex before use. |
    +| **file/index.ts** | `cp`, `ls`, `wc`, `file`, `find`, `pgrep` use `runExecFile` with argument arrays; no shell interpolation. |
    +| **spm/index.ts** | All `swift` package/build/test and `which`/`open` calls use `runExecFile` with argument arrays. |
    +| **cocoapods/index.ts** | All `pod` and helper commands (`which`, `ruby`, `gem`) use `runExecFile` with argument arrays. |
    +| **project/index.ts** | `xcodebuild -list`, `find`, `osascript`, and `xcrun swift package` invocations use `runExecFile` or existing `execFile` with arrays. |
    +| **server.ts** | Project detection uses `runExecFile("osascript", ["-e", script])` and `runExecFile("defaults", ["read", ...])`. |
    +| **utils/project.ts** | `getProjectInfo` / `getWorkspaceInfo` use `runExecFile("xcodebuild", args)`. `find` uses `runExecFile`. |
    +
    +## Adding New Tools
    +
    +When adding or modifying code that runs external commands:
    +
    +1. Use `runExecFile(binary, argsArray, options?)` from `src/utils/execFile.js`.
    +2. Build `argsArray` by appending literal strings (e.g. `args.push("-scheme", scheme)`). Never concatenate user input into a single string that is then passed to a shell.
    +3. For working directory, pass `{ cwd: resolvedPath }` in options.
    +4. Do not use `child_process.exec` or construct shell command strings with user or path-derived input.
    
  • src/server.ts+12 15 modified
    @@ -1,8 +1,7 @@
     import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
     import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
    -import { exec } from "child_process";
    -import { promisify } from "util";
     import * as fs from "fs/promises";
    +import { runExecFile } from "./utils/execFile.js";
     import * as fsSync from "fs";
     import * as path from "path";
     import * as os from "os";
    @@ -20,8 +19,6 @@ import { ProjectDirectoryState } from "./utils/projectDirectoryState.js";
     // Load environment variables from .env file
     dotenv.config();
     
    -const execAsync = promisify(exec);
    -
     // Tool registration functions
     import { registerProjectTools } from "./tools/project/index.js";
     import { registerFileTools } from "./tools/file/index.js";
    @@ -173,16 +170,10 @@ export class XcodeServer {
         try {
           // Attempt to get the frontmost Xcode project via AppleScript.
           try {
    -        const { stdout: frontmostProject } = await execAsync(`
    -          osascript -e '
    -            tell application "Xcode"
    -              if it is running then
    -                set projectFile to path of document 1
    -                return POSIX path of projectFile
    -              end if
    -            end tell
    -          '
    -        `);
    +        const { stdout: frontmostProject } = await runExecFile("osascript", [
    +          "-e",
    +          "tell application \"Xcode\"\n  if it is running then\n    set projectFile to path of document 1\n    return POSIX path of projectFile\n  end if\nend tell"
    +        ]);
     
             if (frontmostProject && frontmostProject.trim()) {
               const projectPath = frontmostProject.trim();
    @@ -276,7 +267,13 @@ export class XcodeServer {
     
           // Further fallback: try reading recent projects from Xcode defaults.
           try {
    -        const { stdout: recentProjects } = await execAsync('defaults read com.apple.dt.Xcode IDERecentWorkspaceDocuments || true');
    +        let recentProjects = "";
    +        try {
    +          const result = await runExecFile("defaults", ["read", "com.apple.dt.Xcode", "IDERecentWorkspaceDocuments"]);
    +          recentProjects = result.stdout;
    +        } catch {
    +          // defaults read exits 1 if key missing
    +        }
             if (recentProjects) {
               const projectMatch = recentProjects.match(/= \\"([^"]+)"/);
               if (projectMatch) {
    
  • src/tools/build/index.ts+86 224 modified
    @@ -1,13 +1,17 @@
     import { z } from "zod";
    -import { promisify } from "util";
    -import { exec } from "child_process";
     import * as fs from "fs/promises";
     import * as path from "path";
     import { XcodeServer } from "../../server.js";
     import { ProjectNotFoundError, XcodeServerError, CommandExecutionError, PathAccessError } from "../../utils/errors.js";
     import { getProjectInfo, getWorkspaceInfo } from "../../utils/project.js";
    +import { runExecFile } from "../../utils/execFile.js";
     
    -const execAsync = promisify(exec);
    +function xcodeBuildArgs(projectPath: string, isWorkspace: boolean): string[] {
    +  if (projectPath.endsWith(".xcworkspace") || isWorkspace) {
    +    return ["-workspace", projectPath];
    +  }
    +  return ["-project", projectPath];
    +}
     
     /**
      * Register build and testing tools
    @@ -149,27 +153,12 @@ export function registerBuildTools(server: XcodeServer) {
     
           try {
             const workingDir = server.directoryState.getActiveDirectory();
    -
    -        // Construct the base command
    -        let projectFlag;
             const projectPath = server.activeProject.path;
    +        const args = [...xcodeBuildArgs(projectPath, server.activeProject.isWorkspace ?? false)];
    +        if (scheme) args.push("-scheme", scheme);
    +        args.push("-showdestinations", "-json");
     
    -        // Check if the path ends with .xcworkspace (workspace) or .xcodeproj (project)
    -        if (projectPath.endsWith('.xcworkspace')) {
    -          projectFlag = `-workspace "${projectPath}"`;
    -        } else if (projectPath.endsWith('.xcodeproj')) {
    -          projectFlag = `-project "${projectPath}"`;
    -        } else {
    -          // Fall back to the server's detection mechanism
    -          projectFlag = server.activeProject.isWorkspace ? `-workspace "${projectPath}"` : `-project "${projectPath}"`;
    -        }
    -
    -        const schemeArg = scheme ? `-scheme "${scheme}"` : "";
    -
    -        // Request destinations in a more structured format
    -        const cmd = `cd "${workingDir}" && xcodebuild ${projectFlag} ${schemeArg} -showdestinations -json`;
    -
    -        const { stdout, stderr } = await execAsync(cmd);
    +        const { stdout, stderr } = await runExecFile("xcodebuild", args, { cwd: workingDir });
     
             // Parse destinations from JSON output
             let destinations;
    @@ -217,25 +206,10 @@ export function registerBuildTools(server: XcodeServer) {
     
           try {
             const workingDir = server.directoryState.getActiveDirectory();
    -
    -        // Construct the base command
    -        let projectFlag;
             const projectPath = server.activeProject.path;
    +        const args = [...xcodeBuildArgs(projectPath, server.activeProject.isWorkspace ?? false), "-list", "-json"];
     
    -        // Check if the path ends with .xcworkspace (workspace) or .xcodeproj (project)
    -        if (projectPath.endsWith('.xcworkspace')) {
    -          projectFlag = `-workspace "${projectPath}"`;
    -        } else if (projectPath.endsWith('.xcodeproj')) {
    -          projectFlag = `-project "${projectPath}"`;
    -        } else {
    -          // Fall back to the server's detection mechanism
    -          projectFlag = server.activeProject.isWorkspace ? `-workspace "${projectPath}"` : `-project "${projectPath}"`;
    -        }
    -
    -        // Request schemes in a more structured format
    -        const cmd = `cd "${workingDir}" && xcodebuild ${projectFlag} -list -json`;
    -
    -        const { stdout, stderr } = await execAsync(cmd);
    +        const { stdout, stderr } = await runExecFile("xcodebuild", args, { cwd: workingDir });
     
             // Parse schemes from JSON output
             let schemeInfo;
    @@ -335,37 +309,19 @@ export function registerBuildTools(server: XcodeServer) {
               throw new XcodeServerError("No scheme specified and no schemes found in the project");
             }
     
    -        // Construct the base command
    -        let projectFlag;
             const projectPath = server.activeProject.path;
    -
    -        // Check if the path ends with .xcworkspace (workspace) or .xcodeproj (project)
    -        if (projectPath.endsWith('.xcworkspace')) {
    -          projectFlag = `-workspace "${projectPath}"`;
    -        } else if (projectPath.endsWith('.xcodeproj')) {
    -          projectFlag = `-project "${projectPath}"`;
    -        } else {
    -          // Fall back to the server's detection mechanism
    -          projectFlag = server.activeProject.isWorkspace ? `-workspace "${projectPath}"` : `-project "${projectPath}"`;
    -        }
    -
    -        // Add configuration if provided
    -        const configFlag = configuration ? `-configuration "${configuration}"` : "";
    -
    +        const args = [...xcodeBuildArgs(projectPath, server.activeProject.isWorkspace ?? false), "-scheme", schemeToUse];
    +        if (configuration) args.push("-configuration", configuration);
             let sanitizedDerivedDataPath: string | undefined;
             if (derivedDataPath) {
               const resolvedPath = server.pathManager.normalizePath(derivedDataPath);
               server.pathManager.validatePathForWriting(resolvedPath);
               sanitizedDerivedDataPath = resolvedPath;
             }
    +        if (sanitizedDerivedDataPath) args.push("-derivedDataPath", sanitizedDerivedDataPath);
    +        args.push("clean");
     
    -        // Add derived data path if provided
    -        const derivedDataFlag = sanitizedDerivedDataPath ? `-derivedDataPath "${sanitizedDerivedDataPath}"` : "";
    -
    -        // Build the clean command
    -        const cmd = `cd "${workingDir}" && xcodebuild ${projectFlag} -scheme "${schemeToUse}" ${configFlag} ${derivedDataFlag} clean`;
    -
    -        const { stdout, stderr } = await execAsync(cmd);
    +        const { stdout, stderr } = await runExecFile("xcodebuild", args, { cwd: workingDir });
     
             return {
               content: [{
    @@ -442,27 +398,16 @@ export function registerBuildTools(server: XcodeServer) {
               throw new XcodeServerError(`Invalid configuration "${configuration}". Available configurations: ${projectInfo.configurations.join(", ")}`);
             }
     
    -        // Construct the base command
    -        let projectFlag;
             const projectPath = server.activeProject.path;
    +        const args = [
    +          ...xcodeBuildArgs(projectPath, server.activeProject.isWorkspace ?? false),
    +          "-scheme", scheme,
    +          "-configuration", configuration,
    +        ];
    +        if (destination) args.push("-destination", destination);
    +        args.push("-archivePath", resolvedArchivePath, "archive");
     
    -        // Check if the path ends with .xcworkspace (workspace) or .xcodeproj (project)
    -        if (projectPath.endsWith('.xcworkspace')) {
    -          projectFlag = `-workspace "${projectPath}"`;
    -        } else if (projectPath.endsWith('.xcodeproj')) {
    -          projectFlag = `-project "${projectPath}"`;
    -        } else {
    -          // Fall back to the server's detection mechanism
    -          projectFlag = server.activeProject.isWorkspace ? `-workspace "${projectPath}"` : `-project "${projectPath}"`;
    -        }
    -
    -        // Add destination if provided
    -        const destinationFlag = destination ? `-destination "${destination}"` : "";
    -
    -        // Build the archive command
    -        const cmd = `cd "${workingDir}" && xcodebuild ${projectFlag} -scheme "${scheme}" -configuration "${configuration}" ${destinationFlag} -archivePath "${resolvedArchivePath}" archive`;
    -
    -        const { stdout, stderr } = await execAsync(cmd);
    +        const { stdout, stderr } = await runExecFile("xcodebuild", args, { cwd: workingDir });
     
             // Check if export options plist is provided for follow-up instructions
             let followUpInstructions = "";
    @@ -523,8 +468,7 @@ export function registerBuildTools(server: XcodeServer) {
      */
     async function findBestAvailableSimulator(runtimePrefix = 'iOS') {
       try {
    -    // Get list of all simulators in JSON format
    -    const { stdout: simulatorList } = await execAsync('xcrun simctl list --json');
    +    const { stdout: simulatorList } = await runExecFile("xcrun", ["simctl", "list", "--json"]);
         const simulators = JSON.parse(simulatorList);
     
         // Find the latest runtime that matches our prefix
    @@ -606,49 +550,30 @@ async function analyzeFile(server: XcodeServer, filePath: string, options: { sdk
           throw new XcodeServerError(`Scheme '${options.scheme}' not found. Available schemes: ${info.schemes.join(', ')}`);
         }
     
    -    let destinationFlag = '';
    +    const projectPath = server.activeProject.path;
    +    const workingDir = server.directoryState.getActiveDirectory();
    +    const relativeFilePath = path.relative(workingDir, filePath);
    +    if (relativeFilePath.startsWith("..")) {
    +      throw new XcodeServerError(`File is outside the project directory: ${filePath}`);
    +    }
     
    -    // If SDK is provided, use it directly
    +    const args = [
    +      ...xcodeBuildArgs(projectPath, server.activeProject.isWorkspace ?? false),
    +      "-scheme", scheme,
    +    ];
         if (options.sdk) {
    -      destinationFlag = `-sdk ${options.sdk}`;
    +      args.push("-sdk", options.sdk);
         } else {
    -      // Otherwise, find a suitable simulator
           const simulator = await findBestAvailableSimulator();
           if (!simulator) {
             throw new XcodeServerError("No available iOS simulators found. Please install at least one iOS simulator or specify an SDK.");
           }
    -
    -      destinationFlag = `-destination "platform=iOS Simulator,id=${simulator.udid}"`;
    -    }
    -
    -    // Build the analyze command
    -    let projectFlag;
    -    const projectPath = server.activeProject.path;
    -
    -    // Check if the path ends with .xcworkspace (workspace) or .xcodeproj (project)
    -    if (projectPath.endsWith('.xcworkspace')) {
    -      projectFlag = `-workspace "${projectPath}"`;
    -    } else if (projectPath.endsWith('.xcodeproj')) {
    -      projectFlag = `-project "${projectPath}"`;
    -    } else {
    -      // Fall back to the server's detection mechanism
    -      projectFlag = server.activeProject.isWorkspace ? `-workspace "${projectPath}"` : `-project "${projectPath}"`;
    -    }
    -
    -    // Get the active directory from ProjectDirectoryState for the working directory
    -    const workingDir = server.directoryState.getActiveDirectory();
    -
    -    // Get the relative path to the file from the working directory
    -    const relativeFilePath = path.relative(workingDir, filePath);
    -    if (relativeFilePath.startsWith('..')) {
    -      throw new XcodeServerError(`File is outside the project directory: ${filePath}`);
    +      args.push("-destination", `platform=iOS Simulator,id=${simulator.udid}`);
         }
    -
    -    // Build the analyze command with more options for better analysis
    -    const cmd = `cd "${workingDir}" && xcodebuild ${projectFlag} -scheme "${scheme}" ${destinationFlag} analyze -quiet -analyzer-output html "${relativeFilePath}"`;
    +    args.push("analyze", "-quiet", "-analyzer-output", "html", relativeFilePath);
     
         try {
    -      const { stdout, stderr } = await execAsync(cmd);
    +      const { stdout, stderr } = await runExecFile("xcodebuild", args, { cwd: workingDir });
     
           // Check for common warning patterns
           let formattedOutput = `Analysis of ${path.basename(filePath)} completed.\n\n`;
    @@ -747,17 +672,14 @@ async function buildProject(server: XcodeServer, configuration: string, scheme:
       try {
         // Different command for workspace vs project vs SPM
         if (server.activeProject.isSPMProject) {
    -      // For SPM projects, we use swift build
    -      const buildConfig = configuration.toLowerCase() === 'release' ? '--configuration release' : '';
           try {
             // Use the active directory from ProjectDirectoryState for the working directory
             const workingDir = server.directoryState.getActiveDirectory();
     
    -        // Add jobs parameter if specified
    -        const jobsArg = options.jobs ? `--jobs ${options.jobs}` : '';
    -
    -        const cmd = `cd "${workingDir}" && swift build ${buildConfig} ${jobsArg}`;
    -        const { stdout, stderr } = await execAsync(cmd);
    +        const swiftArgs = ["build"];
    +        if (configuration.toLowerCase() === "release") swiftArgs.push("--configuration", "release");
    +        if (options.jobs) swiftArgs.push("--jobs", String(options.jobs));
    +        const { stdout, stderr } = await runExecFile("swift", swiftArgs, { cwd: workingDir });
             return {
               content: [{
                 type: "text",
    @@ -792,46 +714,24 @@ async function buildProject(server: XcodeServer, configuration: string, scheme:
           throw new XcodeServerError(`Invalid scheme "${scheme}". Available schemes: ${projectInfo.schemes.join(", ")}`);
         }
     
    -    // Use -workspace for workspace projects, -project for regular projects
    -    // Check if the file exists and has the correct extension
    -    let projectFlag;
    -
    -    // Check if the path ends with .xcworkspace (workspace) or .xcodeproj (project)
    -    if (projectPath.endsWith('.xcworkspace')) {
    -      projectFlag = `-workspace "${projectPath}"`;
    -    } else if (projectPath.endsWith('.xcodeproj')) {
    -      projectFlag = `-project "${projectPath}"`;
    -    } else {
    -      // Fall back to the server's detection mechanism
    -      projectFlag = server.activeProject.isWorkspace ? `-workspace "${projectPath}"` : `-project "${projectPath}"`;
    -    }
    -
    -    // Handle destination or SDK specification
    -    let destinationOrSdkFlag = '';
    -    if (options.destination) {
    -      destinationOrSdkFlag = `-destination "${options.destination}"`;
    -    } else if (options.sdk) {
    -      destinationOrSdkFlag = `-sdk ${options.sdk}`;
    -    } else {
    -      // Try to find a suitable simulator if no destination or SDK is specified
    +    const workingDir = server.directoryState.getActiveDirectory();
    +    const args = [
    +      ...xcodeBuildArgs(projectPath, server.activeProject.isWorkspace ?? false),
    +      "-scheme", scheme,
    +      "-configuration", configuration,
    +    ];
    +    if (options.destination) args.push("-destination", options.destination);
    +    else if (options.sdk) args.push("-sdk", options.sdk);
    +    else {
           const simulator = await findBestAvailableSimulator();
    -      if (simulator) {
    -        destinationOrSdkFlag = `-destination "platform=iOS Simulator,id=${simulator.udid}"`;
    -      }
    +      if (simulator) args.push("-destination", `platform=iOS Simulator,id=${simulator.udid}`);
         }
    -
    -    // Handle additional options
    -    const jobsFlag = options.jobs ? `-jobs ${options.jobs}` : '';
    -    const derivedDataFlag = sanitizedDerivedDataPath ? `-derivedDataPath "${sanitizedDerivedDataPath}"` : '';
    -
    -    // Use the active directory from ProjectDirectoryState for the working directory
    -    const workingDir = server.directoryState.getActiveDirectory();
    -
    -    // More advanced build command with enhanced options
    -    const cmd = `cd "${workingDir}" && xcodebuild ${projectFlag} -scheme "${scheme}" -configuration "${configuration}" ${destinationOrSdkFlag} ${jobsFlag} ${derivedDataFlag} build -quiet`;
    +    if (options.jobs) args.push("-jobs", String(options.jobs));
    +    if (sanitizedDerivedDataPath) args.push("-derivedDataPath", sanitizedDerivedDataPath);
    +    args.push("build", "-quiet");
     
         try {
    -      const { stdout, stderr } = await execAsync(cmd);
    +      const { stdout, stderr } = await runExecFile("xcodebuild", args, { cwd: workingDir });
     
           // Check for known error patterns
           if (stderr && (
    @@ -909,81 +809,43 @@ async function runTests(server: XcodeServer, options: {
           sanitizedResultBundlePath = resolvedPath;
         }
     
    -    // Build the command with all the provided options
    -    let projectFlag;
         const projectPath = server.activeProject.path;
    -
    -    // Check if the path ends with .xcworkspace (workspace) or .xcodeproj (project)
    -    if (projectPath.endsWith('.xcworkspace')) {
    -      projectFlag = `-workspace "${projectPath}"`;
    -    } else if (projectPath.endsWith('.xcodeproj')) {
    -      projectFlag = `-project "${projectPath}"`;
    -    } else {
    -      // Fall back to the server's detection mechanism
    -      projectFlag = server.activeProject.isWorkspace ? `-workspace "${projectPath}"` : `-project "${projectPath}"`;
    -    }
    -
    -    // If scheme is provided, use it, otherwise we need to figure out a scheme
    -    let schemeFlag = '';
    +    let schemeToUse: string;
         if (options.scheme) {
    -      schemeFlag = `-scheme "${options.scheme}"`;
    +      schemeToUse = options.scheme;
         } else {
    -      // Try to get schemes from the project info
    -      try {
    -        const info = server.activeProject.isWorkspace ?
    -          await getWorkspaceInfo(server.activeProject.path) :
    -          await getProjectInfo(server.activeProject.path);
    -
    -        if (info.schemes && info.schemes.length > 0) {
    -          schemeFlag = `-scheme "${info.schemes[0]}"`;
    -        } else {
    -          throw new XcodeServerError("No schemes found in the project. Please specify a scheme for testing.");
    -        }
    -      } catch (error) {
    -        throw new XcodeServerError("Failed to determine scheme for testing. Please specify a scheme.");
    +      const info = server.activeProject.isWorkspace
    +        ? await getWorkspaceInfo(server.activeProject.path)
    +        : await getProjectInfo(server.activeProject.path);
    +      if (info.schemes && info.schemes.length > 0) {
    +        schemeToUse = info.schemes[0];
    +      } else {
    +        throw new XcodeServerError("No schemes found in the project. Please specify a scheme for testing.");
           }
         }
     
    -    // Handle testPlan option
    -    const testPlanFlag = options.testPlan ? `-testPlan "${options.testPlan}"` : "";
    -
    -    // Handle destination option or find a suitable simulator
    -    let destinationFlag = '';
    -    if (options.destination) {
    -      destinationFlag = `-destination "${options.destination}"`;
    -    } else {
    -      // Try to find a suitable simulator
    +    const args = [
    +      ...xcodeBuildArgs(projectPath, server.activeProject.isWorkspace ?? false),
    +      "-scheme", schemeToUse,
    +    ];
    +    if (options.destination) args.push("-destination", options.destination);
    +    else {
           const simulator = await findBestAvailableSimulator();
    -      if (simulator) {
    -        destinationFlag = `-destination "platform=iOS Simulator,id=${simulator.udid}"`;
    -      }
    +      if (simulator) args.push("-destination", `platform=iOS Simulator,id=${simulator.udid}`);
         }
    -
    -    // Handle only-testing flags
    -    const onlyTestingFlags = options.onlyTesting && options.onlyTesting.length > 0
    -      ? options.onlyTesting.map(test => `-only-testing:${test}`).join(' ')
    -      : '';
    -
    -    // Handle skip-testing flags
    -    const skipTestingFlags = options.skipTesting && options.skipTesting.length > 0
    -      ? options.skipTesting.map(test => `-skip-testing:${test}`).join(' ')
    -      : '';
    -
    -    // Handle result bundle path
    -    const resultBundleFlag = sanitizedResultBundlePath
    -      ? `-resultBundlePath "${sanitizedResultBundlePath}"`
    -      : '';
    -
    -    // Handle code coverage
    -    const codeCoverageFlag = options.enableCodeCoverage === true
    -      ? '-enableCodeCoverage YES'
    -      : '';
    -
    -    // Build the full command
    -    const cmd = `cd "${workingDir}" && xcodebuild ${projectFlag} ${schemeFlag} ${destinationFlag} ${testPlanFlag} ${onlyTestingFlags} ${skipTestingFlags} ${resultBundleFlag} ${codeCoverageFlag} test`;
    +    if (options.testPlan) args.push("-testPlan", options.testPlan);
    +    if (options.onlyTesting?.length) {
    +      for (const t of options.onlyTesting) args.push("-only-testing:" + t);
    +    }
    +    if (options.skipTesting?.length) {
    +      for (const t of options.skipTesting) args.push("-skip-testing:" + t);
    +    }
    +    if (sanitizedResultBundlePath) args.push("-resultBundlePath", sanitizedResultBundlePath);
    +    if (options.enableCodeCoverage === true) args.push("-enableCodeCoverage", "YES");
    +    args.push("test");
     
         try {
    -      const { stdout, stderr } = await execAsync(cmd);
    +      const { stdout, stderr } = await runExecFile("xcodebuild", args, { cwd: workingDir });
     
           // Check for test failures
           const hasFailures = stdout.includes("** TEST FAILED **") || stderr.includes("** TEST FAILED **");
    
  • src/tools/cocoapods/index.ts+30 97 modified
    @@ -1,12 +1,9 @@
     import { z } from "zod";
    -import { promisify } from "util";
    -import { exec } from "child_process";
     import * as fs from "fs/promises";
     import * as path from "path";
     import { XcodeServer } from "../../server.js";
     import { ProjectNotFoundError, XcodeServerError, CommandExecutionError, PathAccessError } from "../../utils/errors.js";
    -
    -const execAsync = promisify(exec);
    +import { runExecFile } from "../../utils/execFile.js";
     
     interface GemIssue {
       type: string;
    @@ -51,11 +48,8 @@ async function getPodfileLockVersion(podfileLockPath: string): Promise<string |
      */
     async function checkCocoaPodsInstallation(): Promise<CocoaPodsCheck> {
       try {
    -    // Check if pod command exists
    -    await execAsync('which pod');
    -
    -    // Check CocoaPods version
    -    const { stdout } = await execAsync('pod --version');
    +    await runExecFile("which", ["pod"]);
    +    const { stdout } = await runExecFile("pod", ["--version"]);
         return { installed: true, version: stdout.trim() };
       } catch (error) {
         return { installed: false, error };
    @@ -111,8 +105,8 @@ async function checkVersionCompatibility(projectRoot: string): Promise<void> {
      */
     async function checkRubyEnvironment(): Promise<RubyCheck> {
       try {
    -    const { stdout: rubyVersion } = await execAsync('ruby --version');
    -    const { stdout: gemList } = await execAsync('gem list');
    +    const { stdout: rubyVersion } = await runExecFile("ruby", ["--version"]);
    +    const { stdout: gemList } = await runExecFile("gem", ["list"]);
     
         const issues: GemIssue[] = [];
     
    @@ -223,7 +217,7 @@ async function checkOutdatedPods(projectDir: string): Promise<PodInfo[]> {
         const installedPods = await extractInstalledPods(podfileLockPath);
     
         // Run pod outdated to get the list of outdated pods
    -    const { stdout } = await execAsync('pod outdated --no-repo-update', { cwd: projectDir });
    +    const { stdout } = await runExecFile("pod", ["outdated", "--no-repo-update"], { cwd: projectDir });
     
         // Parse the output to identify outdated pods and their latest versions
         const outdatedPods = new Map<string, string>();
    @@ -351,26 +345,12 @@ export function registerCocoaPodsTools(server: XcodeServer) {
                 LC_ALL: 'en_US.UTF-8'
               };
     
    -          // Build the command with appropriate flags
    -          let cmd = 'pod install';
    -          if (repoUpdate) {
    -            cmd += ' --repo-update';
    -          } else {
    -            cmd += ' --no-repo-update';
    -          }
    -
    -          if (cleanInstall) {
    -            cmd += ' --clean-install';
    -          }
    -
    -          if (verbose) {
    -            cmd += ' --verbose';
    -          }
    -
    -          const { stdout, stderr } = await execAsync(cmd, {
    -            cwd: activeDirectory,
    -            env
    -          });
    +          const installArgs = ["install"];
    +          if (repoUpdate) installArgs.push("--repo-update");
    +          else installArgs.push("--no-repo-update");
    +          if (cleanInstall) installArgs.push("--clean-install");
    +          if (verbose) installArgs.push("--verbose");
    +          const { stdout, stderr } = await runExecFile("pod", installArgs, { cwd: activeDirectory, env });
     
               return {
                 content: [{
    @@ -520,38 +500,13 @@ export function registerCocoaPodsTools(server: XcodeServer) {
                 LC_ALL: 'en_US.UTF-8'
               };
     
    -          // Build the command
    -          let cmd = 'pod update';
    -
    -          // Add specific pods to update
    -          if (pods && pods.length > 0) {
    -            cmd += ' ' + pods.map(pod => pod.trim()).join(' ');
    -          }
    -
    -          // Add repo update flag
    -          if (!repoUpdate) {
    -            cmd += ' --no-repo-update';
    -          }
    -
    -          // Add excluded pods
    -          if (excludePods && excludePods.length > 0) {
    -            cmd += ` --exclude-pods=${excludePods.join(',')}`;
    -          }
    -
    -          // Add clean install flag
    -          if (cleanInstall) {
    -            cmd += ' --clean-install';
    -          }
    -
    -          // Add sources
    -          if (sources && sources.length > 0) {
    -            cmd += ` --sources=${sources.join(',')}`;
    -          }
    -
    -          const { stdout, stderr } = await execAsync(cmd, {
    -            cwd: activeDirectory,
    -            env
    -          });
    +          const updateArgs = ["update"];
    +          if (pods?.length) updateArgs.push(...pods.map((p) => p.trim()));
    +          if (!repoUpdate) updateArgs.push("--no-repo-update");
    +          if (excludePods?.length) updateArgs.push(`--exclude-pods=${excludePods.join(",")}`);
    +          if (cleanInstall) updateArgs.push("--clean-install");
    +          if (sources?.length) updateArgs.push(`--sources=${sources.join(",")}`);
    +          const { stdout, stderr } = await runExecFile("pod", updateArgs, { cwd: activeDirectory, env });
     
               return {
                 content: [{
    @@ -648,20 +603,10 @@ export function registerCocoaPodsTools(server: XcodeServer) {
             }
     
             try {
    -          // Build command with options
    -          let cmd = 'pod outdated';
    -
    -          if (ignorePrerelease) {
    -            cmd += ' --ignore-prerelease';
    -          }
    -
    -          if (!repoUpdate) {
    -            cmd += ' --no-repo-update';
    -          }
    -
    -          const { stdout, stderr } = await execAsync(cmd, {
    -            cwd: activeDirectory
    -          });
    +          const outdatedArgs = ["outdated"];
    +          if (ignorePrerelease) outdatedArgs.push("--ignore-prerelease");
    +          if (!repoUpdate) outdatedArgs.push("--no-repo-update");
    +          const { stdout, stderr } = await runExecFile("pod", outdatedArgs, { cwd: activeDirectory });
     
               // Parse the outdated pods information
               const outdatedPods: PodInfo[] = [];
    @@ -727,18 +672,10 @@ export function registerCocoaPodsTools(server: XcodeServer) {
         },
         async ({ verbose = false, silent = false }) => {
           try {
    -        // Build command with options
    -        let cmd = 'pod repo update';
    -
    -        if (verbose) {
    -          cmd += ' --verbose';
    -        }
    -
    -        if (silent) {
    -          cmd += ' --silent';
    -        }
    -
    -        const { stdout, stderr } = await execAsync(cmd);
    +        const repoArgs = ["repo", "update"];
    +        if (verbose) repoArgs.push("--verbose");
    +        if (silent) repoArgs.push("--silent");
    +        const { stdout, stderr } = await runExecFile("pod", repoArgs);
     
             return {
               content: [{
    @@ -785,9 +722,7 @@ export function registerCocoaPodsTools(server: XcodeServer) {
             }
     
             try {
    -          const { stdout, stderr } = await execAsync('pod deintegrate', {
    -            cwd: activeDirectory
    -          });
    +          const { stdout, stderr } = await runExecFile("pod", ["deintegrate"], { cwd: activeDirectory });
     
               return {
                 content: [{
    @@ -889,7 +824,7 @@ export function registerCocoaPodsTools(server: XcodeServer) {
             let repoInfo = null;
             if (cocoaPodsCheck.installed) {
               try {
    -            const { stdout } = await execAsync('pod repo list');
    +            const { stdout } = await runExecFile("pod", ["repo", "list"]);
                 repoInfo = stdout.trim();
               } catch {
                 // Ignore repo listing errors
    @@ -953,9 +888,7 @@ export function registerCocoaPodsTools(server: XcodeServer) {
             }
     
             try {
    -          const { stdout, stderr } = await execAsync('pod init', {
    -            cwd: activeDirectory
    -          });
    +          const { stdout, stderr } = await runExecFile("pod", ["init"], { cwd: activeDirectory });
     
               // Read the generated Podfile
               let podfileContent = "";
    
  • src/tools/file/index.ts+35 64 modified
    @@ -5,10 +5,7 @@ import * as path from "path";
     import { XcodeServer } from "../../server.js";
     import { ProjectNotFoundError, PathAccessError, FileOperationError, CommandExecutionError } from "../../utils/errors.js";
     import { getMimeTypeForExtension, listDirectory, expandPath } from "../../utils/file.js";
    -import { promisify } from "util";
    -import { exec } from "child_process";
    -
    -const execAsync = promisify(exec);
    +import { runExecFile } from "../../utils/execFile.js";
     
     /**
      * Helper function to format file size in human-readable format
    @@ -65,22 +62,15 @@ async function getFileInfo(filePath: string): Promise<FileInfo> {
       else if (stats.isDirectory()) type = 'directory';
       else if (stats.isSymbolicLink()) type = 'symlink';
     
    -  // Try to get owner info (this might fail in some environments)
    -  let owner = undefined;
    -  try {
    -    const { stdout } = await execAsync(`ls -l "${filePath}" | awk '{print $3}'`);
    -    owner = stdout.trim();
    -  } catch {
    -    // Ignore errors, owner will remain undefined
    -  }
    -
    -  // Get permissions
    -  let permissions = undefined;
    +  let owner: string | undefined;
    +  let permissions: string | undefined;
       try {
    -    const { stdout } = await execAsync(`ls -la "${filePath}" | awk '{print $1}'`);
    -    permissions = stdout.trim();
    +    const { stdout } = await runExecFile("ls", ["-l", filePath]);
    +    const parts = stdout.trim().split(/\s+/);
    +    if (parts.length >= 3) permissions = parts[0];
    +    if (parts.length >= 4) owner = parts[2];
       } catch {
    -    // Ignore errors, permissions will remain undefined
    +    // Ignore errors
       }
     
       return {
    @@ -329,10 +319,9 @@ export function registerFileTools(server: XcodeServer) {
             }
     
             if (isDirectory) {
    -          // Use cp with recursive flag for directories
    -          const { stdout, stderr } = await execAsync(`cp -R "${resolvedSource}" "${targetPath}"`);
    +          const { stderr } = await runExecFile("cp", ["-R", resolvedSource, targetPath]);
               if (stderr) {
    -            throw new FileOperationError('copy directory', resolvedSource, new Error(stderr));
    +            throw new FileOperationError("copy directory", resolvedSource, new Error(stderr));
               }
             } else {
               // Copy file
    @@ -736,13 +725,13 @@ export function registerFileTools(server: XcodeServer) {
               }
     
               // For text files, try to show encoding and line count
    -          if (mimeType && mimeType.startsWith('text/') && info.size && info.size < 10 * 1024 * 1024) {
    +          if (mimeType && mimeType.startsWith("text/") && info.size && info.size < 10 * 1024 * 1024) {
                 try {
    -              const { stdout: wc } = await execAsync(`wc -l "${resolvedPath}" | awk '{print $1}'`);
    -              additionalInfo += `Line Count: ${wc.trim()}\n`;
    -
    -              const { stdout: file } = await execAsync(`file -b "${resolvedPath}"`);
    -              additionalInfo += `File Type: ${file.trim()}\n`;
    +              const { stdout: wc } = await runExecFile("wc", ["-l", resolvedPath]);
    +              const lineCount = wc.trim().split(/\s+/)[0] ?? "";
    +              additionalInfo += `Line Count: ${lineCount}\n`;
    +              const { stdout: fileOut } = await runExecFile("file", ["-b", resolvedPath]);
    +              additionalInfo += `File Type: ${fileOut.trim()}\n`;
                 } catch {
                   // Ignore errors for these extra info commands
                 }
    @@ -811,38 +800,13 @@ export function registerFileTools(server: XcodeServer) {
               throw new FileOperationError('find', resolvedPath, new Error('Path is not a directory'));
             }
     
    -        // Use find command with appropriate options
    -        let findCmd = `find "${resolvedPath}" -type f`;
    -
    -        // Add maxDepth if specified
    -        if (maxDepth !== undefined) {
    -          findCmd += ` -maxdepth ${maxDepth}`;
    -        }
    -
    -        // Exclude hidden files/dirs if not showing hidden
    -        if (!showHidden) {
    -          findCmd += ` -not -path "*/\\.*"`;
    -        }
    -
    -        // Add pattern matching
    -        if (pattern) {
    -          // Convert glob pattern to find-compatible pattern
    -          if (pattern.includes('*') || pattern.includes('?')) {
    -            // For simple glob patterns, use -name
    -            if (!pattern.includes('/**/')) {
    -              findCmd += ` -name "${pattern}"`;
    -            } else {
    -              // For more complex patterns with ** (any depth), we need to post-process
    -              // Remove the ** handling and filter results after
    -            }
    -          } else {
    -            // For exact matches, use -name
    -            findCmd += ` -name "${pattern}"`;
    -          }
    -        }
    +        const findArgs = [resolvedPath, "-type", "f"];
    +        if (maxDepth !== undefined) findArgs.push("-maxdepth", String(maxDepth));
    +        if (!showHidden) findArgs.push("-not", "-path", "*/\\.*");
    +        if (pattern && !pattern.includes("/**/")) findArgs.push("-name", pattern);
     
             try {
    -          const { stdout, stderr } = await execAsync(findCmd);
    +          const { stdout, stderr } = await runExecFile("find", findArgs);
     
               if (stderr) {
                 console.error(`Warning from find command: ${stderr}`);
    @@ -1050,13 +1014,15 @@ export function registerFileTools(server: XcodeServer) {
               searchRegex = new RegExp(escapedText, caseSensitive ? 'g' : 'gi');
             }
     
    -        // Find matching files
    -        const findCmd = `find "${resolvedPath}" -type f ${includeHidden ? '' : '-not -path "*/\\.*"'} -name "${pattern}"${maxResults ? ` | head -n ${maxResults}` : ''}`;
    +        const findArgs = [resolvedPath, "-type", "f"];
    +        if (!includeHidden) findArgs.push("-not", "-path", "*/\\.*");
    +        findArgs.push("-name", pattern);
     
             let files: string[];
             try {
    -          const { stdout } = await execAsync(findCmd);
    -          files = stdout.trim().split('\n').filter(Boolean);
    +          const { stdout } = await runExecFile("find", findArgs);
    +          files = stdout.trim().split("\n").filter(Boolean);
    +          if (maxResults) files = files.slice(0, maxResults);
             } catch (error) {
               throw new CommandExecutionError(
                 'find',
    @@ -1429,9 +1395,14 @@ async function updateProjectReferences(projectRoot: string, filePath: string) {
         // This is a more advanced approach that requires Xcode to be running
         try {
           // Check if Xcode is running
    -      const { stdout: isRunning } = await execAsync('pgrep -x Xcode || echo "not running"');
    -
    -      if (isRunning.trim() === 'not running') {
    +      let isRunning = false;
    +      try {
    +        await runExecFile("pgrep", ["-x", "Xcode"]);
    +        isRunning = true;
    +      } catch {
    +        // pgrep exits 1 when no process found
    +      }
    +      if (!isRunning) {
             console.error('Xcode is not running. Cannot automatically add file to project.');
             return;
           }
    
  • src/tools/project/index.ts+26 46 modified
    @@ -2,13 +2,12 @@ import { z } from "zod";
     import * as fs from "fs/promises";
     import * as fsSync from "fs";
     import * as path from "path";
    +import { execFile } from "child_process";
     import { promisify } from "util";
    -import { exec, execFile } from "child_process";
     import { XcodeServer } from "../../server.js";
     import { getProjectInfo } from "../../utils/project.js";
     import { ProjectNotFoundError, PathAccessError, FileOperationError, CommandExecutionError } from "../../utils/errors.js";
    -
    -const execAsync = promisify(exec);
    +import { runExecFile } from "../../utils/execFile.js";
     
     /**
      * Interface for workspace document
    @@ -181,10 +180,12 @@ async function getProjectConfiguration(projectPath: string): Promise<ProjectConf
         // Get project schemes
         try {
           // Determine if this is a workspace or regular project
    -      const isWorkspace = projectPath.endsWith('.xcworkspace');
    -      const flag = isWorkspace ? '-workspace' : '-project';
    -
    -      const { stdout: schemesOutput } = await execAsync(`xcodebuild ${flag} "${projectPath}" -list`);
    +      const isWorkspace = projectPath.endsWith(".xcworkspace");
    +      const { stdout: schemesOutput } = await runExecFile("xcodebuild", [
    +        isWorkspace ? "-workspace" : "-project",
    +        projectPath,
    +        "-list"
    +      ]);
     
           // Parse schemes
           const schemesMatch = schemesOutput.match(/Schemes:\s+((?:.+\s*)+)/);
    @@ -493,8 +494,12 @@ export function registerProjectTools(server: XcodeServer) {
             if (includeSPM) {
               try {
                 // Find directories containing Package.swift
    -            const findCmd = `find "${searchDir}" -name "Package.swift" -type f -not -path "*/\\.*/"`;
    -            const { stdout: spmOutput } = await execAsync(findCmd);
    +            const { stdout: spmOutput } = await runExecFile("find", [
    +              searchDir,
    +              "-name", "Package.swift",
    +              "-type", "f",
    +              "-not", "-path", "*/\\.*"
    +            ]);
     
                 if (spmOutput.trim()) {
                   spmProjects = spmOutput.trim().split('\n')
    @@ -958,8 +963,7 @@ export function registerProjectTools(server: XcodeServer) {
             await fs.writeFile(tempScriptPath, scriptContent, "utf-8");
     
             try {
    -          // Execute the AppleScript
    -          const { stdout, stderr } = await execAsync(`osascript "${tempScriptPath}"`);
    +          const { stdout, stderr } = await runExecFile("osascript", [tempScriptPath]);
     
               // Clean up the temporary script file
               await fs.unlink(tempScriptPath).catch(() => {});
    @@ -1287,24 +1291,13 @@ export function registerProjectTools(server: XcodeServer) {
                 throw new Error(`Unsupported template: ${template}`);
             }
     
    -        // Build the command to create the project
    -        let cmd = `cd "${resolvedOutputDir}" && xcrun swift package init`;
    -
             // For Swift Package Manager templates, use swift package init
             if (template === "cross-platform-framework" || template === "cross-platform-library") {
               const isLibrary = template === "cross-platform-library";
    -          cmd = `cd "${resolvedOutputDir}" && xcrun swift package init --${isLibrary ? 'type library' : 'type framework'}`;
    -
    -          if (!includeTests) {
    -            cmd += " --no-tests";
    -          }
    -
    -          // Execute the command
    -          const { stdout, stderr } = await execAsync(cmd);
    -
    -          // Generate Xcode project from the Swift package
    -          const generateCmd = `cd "${resolvedOutputDir}" && xcrun swift package generate-xcodeproj`;
    -          const { stdout: genStdout, stderr: genStderr } = await execAsync(generateCmd);
    +          const initArgs = ["swift", "package", "init", "--type", isLibrary ? "library" : "framework"];
    +          if (!includeTests) initArgs.push("--no-tests");
    +          const { stdout, stderr } = await runExecFile("xcrun", initArgs, { cwd: resolvedOutputDir });
    +          const { stdout: genStdout, stderr: genStderr } = await runExecFile("xcrun", ["swift", "package", "generate-xcodeproj"], { cwd: resolvedOutputDir });
     
               // Get the path to the generated .xcodeproj
               const projectPath = path.join(resolvedOutputDir, `${name}.xcodeproj`);
    @@ -1334,29 +1327,16 @@ export function registerProjectTools(server: XcodeServer) {
               };
             }
     
    -        // For Xcode templates, use Xcode's template system
    -        // Create a temporary directory for the project
             const projectDir = path.join(resolvedOutputDir, name);
             await fs.mkdir(projectDir, { recursive: true });
     
    -        // Build the Xcode project creation command
    -        cmd = `cd "${resolvedOutputDir}" && xcrun swift package generate-xcodeproj --output "${name}.xcodeproj"`;
    -
    -        // Add organization name if provided
    -        const orgNameArg = organizationName ? `--organization "${organizationName}"` : "";
    -
    -        // Add organization identifier if provided
    -        const orgIdArg = organizationIdentifier ? `--identifier "${organizationIdentifier}"` : "";
    -
    -        // Add language
    -        const langArg = `--language ${language === "swift" ? "Swift" : "Objective-C"}`;
    -
    -        // Add test options
    -        const testArgs = includeTests ? "" : "--no-tests";
    -        const uiTestArgs = includeUITests ? "--include-ui-tests" : "";
    -
    -        // Execute the command
    -        const { stdout, stderr } = await execAsync(cmd);
    +        const genArgs = ["swift", "package", "generate-xcodeproj", "--output", `${name}.xcodeproj`];
    +        if (organizationName) genArgs.push("--organization", organizationName);
    +        if (organizationIdentifier) genArgs.push("--identifier", organizationIdentifier);
    +        genArgs.push("--language", language === "swift" ? "Swift" : "Objective-C");
    +        if (!includeTests) genArgs.push("--no-tests");
    +        if (includeUITests) genArgs.push("--include-ui-tests");
    +        const { stdout, stderr } = await runExecFile("xcrun", genArgs, { cwd: resolvedOutputDir });
     
             // Get the path to the generated .xcodeproj
             const projectPath = path.join(resolvedOutputDir, `${name}.xcodeproj`);
    
  • src/tools/simulator/index.ts+34 40 modified
    @@ -1,12 +1,17 @@
     import { z } from "zod";
    -import { promisify } from "util";
    -import { exec } from "child_process";
     import * as fs from "fs/promises";
     import * as path from "path";
     import { XcodeServer } from "../../server.js";
     import { CommandExecutionError, PathAccessError } from "../../utils/errors.js";
    +import { runExecFile } from "../../utils/execFile.js";
     
    -const execAsync = promisify(exec);
    +const UDID_REGEX = /^[0-9A-Fa-f-]{36}$/;
    +
    +function validateUdid(udid: string): void {
    +  if (!UDID_REGEX.test(udid)) {
    +    throw new Error(`Invalid simulator UDID format: "${udid}". Use list_simulators to get a valid UDID.`);
    +  }
    +}
     
     interface SimulatorInfo {
       name: string;
    @@ -81,7 +86,7 @@ function parseSimulatorDevices(simctlOutput: string): SimulatorInfo[] {
      */
     async function getBootedSimulators(): Promise<SimulatorInfo[]> {
       try {
    -    const { stdout } = await execAsync("xcrun simctl list --json");
    +    const { stdout } = await runExecFile("xcrun", ["simctl", "list", "--json"]);
         const allSimulators = parseSimulatorDevices(stdout);
         return allSimulators.filter(sim => sim.state === 'Booted');
       } catch (error) {
    @@ -162,7 +167,7 @@ export function registerSimulatorTools(server: XcodeServer) {
         },
         async ({ format = "parsed", filterRuntime, filterState, filterName }) => {
           try {
    -      const { stdout, stderr } = await execAsync("xcrun simctl list --json");
    +      const { stdout, stderr } = await runExecFile("xcrun", ["simctl", "list", "--json"]);
     
             if (format === "json") {
           return {
    @@ -258,7 +263,7 @@ export function registerSimulatorTools(server: XcodeServer) {
     
             // If name is provided, we need to find the matching simulator
             if (name && !udid) {
    -          const { stdout } = await execAsync("xcrun simctl list --json");
    +          const { stdout } = await runExecFile("xcrun", ["simctl", "list", "--json"]);
               const simulators = parseSimulatorDevices(stdout);
     
               // Filter by name (case insensitive)
    @@ -304,8 +309,9 @@ export function registerSimulatorTools(server: XcodeServer) {
               udid = matches[0].udid;
             }
     
    -        // Boot the simulator
    -      const { stdout, stderr } = await execAsync(`xcrun simctl boot "${udid}"`);
    +        if (!udid) throw new Error("Simulator UDID could not be determined");
    +        validateUdid(udid);
    +        const { stdout, stderr } = await runExecFile("xcrun", ["simctl", "boot", udid]);
     
             return {
               content: [{
    @@ -376,16 +382,9 @@ export function registerSimulatorTools(server: XcodeServer) {
               throw new Error("Either udid parameter or all=true must be provided");
             }
     
    -        let command;
    -        if (all) {
    -          // Shutdown all simulators
    -          command = "xcrun simctl shutdown all";
    -        } else {
    -          // Shutdown specific simulator
    -          command = `xcrun simctl shutdown "${udid}"`;
    -        }
    -
    -        const { stdout, stderr } = await execAsync(command);
    +        if (!all) validateUdid(udid!);
    +        const args = all ? ["simctl", "shutdown", "all"] : ["simctl", "shutdown", udid!];
    +        const { stdout, stderr } = await runExecFile("xcrun", args);
     
             return {
               content: [{
    @@ -449,8 +448,8 @@ export function registerSimulatorTools(server: XcodeServer) {
               throw new Error(`The app bundle doesn't exist: ${appPath}`);
             }
     
    -        // Install the app
    -        const { stdout, stderr } = await execAsync(`xcrun simctl install "${udid}" "${resolvedAppPath}"`);
    +        validateUdid(udid);
    +        const { stdout, stderr } = await runExecFile("xcrun", ["simctl", "install", udid, resolvedAppPath]);
     
             return {
               content: [{
    @@ -488,18 +487,11 @@ export function registerSimulatorTools(server: XcodeServer) {
         },
         async ({ udid, bundleId, waitForDebugger = false, args = [] }) => {
           try {
    -        // Build the command
    -        let command = `xcrun simctl launch ${waitForDebugger ? '-w' : ''}`;
    -
    -        // Add the UDID and bundle ID
    -        command += ` "${udid}" "${bundleId}"`;
    -
    -        // Add any arguments
    -        if (args.length > 0) {
    -          command += ` ${args.map(arg => `"${arg}"`).join(' ')}`;
    -        }
    -
    -        const { stdout, stderr } = await execAsync(command);
    +        validateUdid(udid);
    +        const launchArgs = ["simctl", "launch"];
    +        if (waitForDebugger) launchArgs.push("-w");
    +        launchArgs.push(udid, bundleId, ...args);
    +        const { stdout, stderr } = await runExecFile("xcrun", launchArgs);
     
             return {
               content: [{
    @@ -552,7 +544,8 @@ export function registerSimulatorTools(server: XcodeServer) {
         },
         async ({ udid, bundleId }) => {
           try {
    -        const { stdout, stderr } = await execAsync(`xcrun simctl terminate "${udid}" "${bundleId}"`);
    +        validateUdid(udid);
    +        const { stdout, stderr } = await runExecFile("xcrun", ["simctl", "terminate", udid, bundleId]);
     
             return {
               content: [{
    @@ -605,7 +598,8 @@ export function registerSimulatorTools(server: XcodeServer) {
               throw new Error(`Invalid URL format: ${url}`);
             }
     
    -        const { stdout, stderr } = await execAsync(`xcrun simctl openurl "${udid}" "${url}"`);
    +        validateUdid(udid);
    +        const { stdout, stderr } = await runExecFile("xcrun", ["simctl", "openurl", udid, url]);
     
             return {
               content: [{
    @@ -645,8 +639,8 @@ export function registerSimulatorTools(server: XcodeServer) {
             const outputDir = path.dirname(resolvedOutputPath);
             await fs.mkdir(outputDir, { recursive: true });
     
    -        // Take the screenshot
    -        const { stdout, stderr } = await execAsync(`xcrun simctl io "${udid}" screenshot "${resolvedOutputPath}"`);
    +        validateUdid(udid);
    +        const { stdout, stderr } = await runExecFile("xcrun", ["simctl", "io", udid, "screenshot", resolvedOutputPath]);
     
             return {
               content: [{
    @@ -687,8 +681,8 @@ export function registerSimulatorTools(server: XcodeServer) {
               throw new Error("You must set confirm=true to reset a simulator. This will erase all content and settings.");
             }
     
    -        // Reset the simulator
    -        const { stdout, stderr } = await execAsync(`xcrun simctl erase "${udid}"`);
    +        validateUdid(udid);
    +        const { stdout, stderr } = await runExecFile("xcrun", ["simctl", "erase", udid]);
     
             return {
               content: [{
    @@ -728,8 +722,8 @@ export function registerSimulatorTools(server: XcodeServer) {
         },
         async ({ udid }) => {
           try {
    -        // Get the list of installed apps
    -        const { stdout, stderr } = await execAsync(`xcrun simctl listapps "${udid}"`);
    +        validateUdid(udid);
    +        const { stdout, stderr } = await runExecFile("xcrun", ["simctl", "listapps", udid]);
     
             // Parse the output to extract app information
             // The output format is a plist-like structure
    
  • src/tools/spm/index.ts+65 185 modified
    @@ -1,12 +1,9 @@
     import { z } from "zod";
    -import { promisify } from "util";
    -import { exec } from "child_process";
     import * as fs from "fs/promises";
     import * as path from "path";
     import { XcodeServer } from "../../server.js";
     import { ProjectNotFoundError, XcodeServerError, CommandExecutionError, PathAccessError, FileOperationError } from "../../utils/errors.js";
    -
    -const execAsync = promisify(exec);
    +import { runExecFile } from "../../utils/execFile.js";
     
     /**
      * Helper function to escape special characters in a string for use in a regular expression
    @@ -223,10 +220,8 @@ export function registerSPMTools(server: XcodeServer) {
                     testingArgs = '--enable-xctest --disable-swift-testing';
                   }
     
    -              // Use absolute paths and proper directory
    -              const cmd = `cd "${packageDirectory}" && swift package init ${typeArg} ${nameArg} ${testingArgs}`.trim();
    -
    -              const { stdout, stderr } = await execAsync(cmd);
    +              const initArgs = ["package", "init", typeArg, nameArg, ...testingArgs.trim().split(/\s+/).filter(Boolean)];
    +              const { stdout, stderr } = await runExecFile("swift", initArgs, { cwd: packageDirectory });
     
                   // If we have an active project, update its info to reflect it's now an SPM project
                   if (server.activeProject) {
    @@ -301,34 +296,28 @@ export function registerSPMTools(server: XcodeServer) {
             }
     
             try {
    -          let dependencyArg = `"${url}"`;
    -
    -          // Handle version requirements
    +          const addArgs = ["package", "add-dependency", url];
               if (version) {
    -            if (version.startsWith('exact:')) {
    -              dependencyArg += ` --exact ${version.split(':')[1].trim()}`;
    -            } else if (version.startsWith('from:')) {
    -              dependencyArg += ` --from ${version.split(':')[1].trim()}`;
    -            } else if (version.startsWith('branch:')) {
    -              dependencyArg += ` --branch ${version.split(':')[1].trim()}`;
    -            } else if (version.startsWith('revision:')) {
    -              dependencyArg += ` --revision ${version.split(':')[1].trim()}`;
    +            if (version.startsWith("exact:")) {
    +              addArgs.push("--exact", version.split(":")[1].trim());
    +            } else if (version.startsWith("from:")) {
    +              addArgs.push("--from", version.split(":")[1].trim());
    +            } else if (version.startsWith("branch:")) {
    +              addArgs.push("--branch", version.split(":")[1].trim());
    +            } else if (version.startsWith("revision:")) {
    +              addArgs.push("--revision", version.split(":")[1].trim());
                 } else {
    -              // Assume it's a "from:" version if not specified
    -              dependencyArg += ` --from ${version}`;
    +              addArgs.push("--from", version);
                 }
               }
    +          if (productName) addArgs.push("--product", productName);
     
    -          const productArg = productName ? ` --product ${productName}` : '';
    -          const cmd = `cd "${activeDirectory}" && swift package add-dependency ${dependencyArg}${productArg}`;
    -
    -          const { stdout, stderr } = await execAsync(cmd);
    +          const { stdout, stderr } = await runExecFile("swift", addArgs, { cwd: activeDirectory });
     
    -          // After adding dependency, run package update unless skipped
    -          let updateOutput = '';
    +          let updateOutput = "";
               if (!skipUpdate) {
                 try {
    -              const { stdout: updateStdout, stderr: updateStderr } = await execAsync('swift package update', { cwd: activeDirectory });
    +              const { stdout: updateStdout, stderr: updateStderr } = await runExecFile("swift", ["package", "update"], { cwd: activeDirectory });
                   updateOutput = `\n\nDependencies updated:\n${updateStdout}${updateStderr ? '\nUpdate errors:\n' + updateStderr : ''}`;
                 } catch (updateError) {
                   updateOutput = `\n\nFailed to update dependencies: ${updateError instanceof Error ? updateError.message : String(updateError)}`;
    @@ -455,7 +444,7 @@ export function registerSPMTools(server: XcodeServer) {
               // Run package update to clean up
               let updateOutput = '';
               try {
    -            const { stdout: updateStdout, stderr: updateStderr } = await execAsync('swift package update', { cwd: activeDirectory });
    +            const { stdout: updateStdout, stderr: updateStderr } = await runExecFile("swift", ["package", "update"], { cwd: activeDirectory });
                 updateOutput = `\n\nDependencies updated:\n${updateStdout}${updateStderr ? '\nUpdate errors:\n' + updateStderr : ''}`;
               } catch (updateError) {
                 updateOutput = `\n\nFailed to update dependencies after removal: ${updateError instanceof Error ? updateError.message : String(updateError)}`;
    @@ -544,7 +533,7 @@ export function registerSPMTools(server: XcodeServer) {
             let updateOutput = '';
     
             try {
    -          const { stdout, stderr } = await execAsync('swift package update', { cwd: packageDir });
    +          const { stdout, stderr } = await runExecFile("swift", ["package", "update"], { cwd: packageDir });
               updateOutput = `\n\nDependencies updated:\n${stdout}${stderr ? '\nUpdate errors:\n' + stderr : ''}`;
             } catch (error) {
               let stderr = '';
    @@ -608,21 +597,10 @@ export function registerSPMTools(server: XcodeServer) {
               }
             }
     
    -        // Build the command
    -        let cmd = `cd "${packageDir}" && swift build --configuration ${configuration}`;
    -
    -        // Add target if specified
    -        if (target) {
    -          cmd += ` --target ${target}`;
    -        }
    -
    -        // Add verbose flag if requested
    -        if (verbose) {
    -          cmd += ` --verbose`;
    -        }
    -
    -        // Execute the command
    -        const { stdout, stderr } = await execAsync(cmd);
    +        const buildArgs = ["build", "--configuration", configuration];
    +        if (target) buildArgs.push("--target", target);
    +        if (verbose) buildArgs.push("--verbose");
    +        const { stdout, stderr } = await runExecFile("swift", buildArgs, { cwd: packageDir });
     
             return {
               content: [{
    @@ -683,26 +661,11 @@ export function registerSPMTools(server: XcodeServer) {
               }
             }
     
    -        // Build the command
    -        let cmd = `cd "${packageDir}" && swift test`;
    -
    -        // Add filter if specified
    -        if (filter) {
    -          cmd += ` --filter "${filter}"`;
    -        }
    -
    -        // Add parallel flag if requested
    -        if (parallel) {
    -          cmd += ` --parallel`;
    -        }
    -
    -        // Add verbose flag if requested
    -        if (verbose) {
    -          cmd += ` --verbose`;
    -        }
    -
    -        // Execute the command
    -        const { stdout, stderr } = await execAsync(cmd);
    +        const testArgs = ["test"];
    +        if (filter) testArgs.push("--filter", filter);
    +        if (parallel) testArgs.push("--parallel");
    +        if (verbose) testArgs.push("--verbose");
    +        const { stdout, stderr } = await runExecFile("swift", testArgs, { cwd: packageDir });
     
             return {
               content: [{
    @@ -790,7 +753,7 @@ export function registerSPMTools(server: XcodeServer) {
             // Get package targets using swift package dump-package
             let packageDump: any = {};
             try {
    -          const { stdout } = await execAsync(`cd "${packageDir}" && swift package dump-package`);
    +          const { stdout } = await runExecFile("swift", ["package", "dump-package"], { cwd: packageDir });
               packageDump = JSON.parse(stdout);
             } catch (error) {
               console.error("Error dumping package:", error);
    @@ -889,31 +852,10 @@ export function registerSPMTools(server: XcodeServer) {
             }
     
             try {
    -          let cmd: string;
    -
    -          // If updating a specific package, use resolve command
    -          if (specificPackage) {
    -            cmd = `cd "${activeDirectory}" && swift package resolve`;
    -
    -            if (version) {
    -              cmd += ` --version "${version}"`;
    -            }
    -
    -            if (branch) {
    -              cmd += ` --branch "${branch}"`;
    -            }
    -
    -            if (revision) {
    -              cmd += ` --revision "${revision}"`;
    -            }
    -
    -            cmd += ` "${specificPackage}"`;
    -          } else {
    -            // Otherwise, use regular update command
    -            cmd = `cd "${activeDirectory}" && swift package update`;
    -          }
    -
    -          const { stdout, stderr } = await execAsync(cmd);
    +          const pkgArgs = specificPackage
    +            ? ["package", "resolve", ...(version ? ["--version", version] : []), ...(branch ? ["--branch", branch] : []), ...(revision ? ["--revision", revision] : []), specificPackage]
    +            : ["package", "update"];
    +          const { stdout, stderr } = await runExecFile("swift", pkgArgs, { cwd: activeDirectory });
     
               // Try to get information about current dependencies
               let dependencyInfo = '';
    @@ -999,12 +941,11 @@ export function registerSPMTools(server: XcodeServer) {
               throw new XcodeServerError("No Package.swift found in the active directory. This project doesn't use Swift Package Manager.");
             }
     
    -        const configArg = configuration ? `--configuration ${configuration}` : '';
    -        const extraArgsStr = extraArgs || '';
    -
    +        const pkgArgs = ["package", command];
    +        if (configuration) pkgArgs.push("--configuration", configuration);
    +        if (extraArgs?.trim()) pkgArgs.push(extraArgs.trim());
             try {
    -          const cmd = `cd "${activeDirectory}" && swift package ${command} ${configArg} ${extraArgsStr}`.trim();
    -          const { stdout, stderr } = await execAsync(cmd);
    +          const { stdout, stderr } = await runExecFile("swift", pkgArgs, { cwd: activeDirectory });
               return {
                 content: [{
                   type: "text",
    @@ -1062,35 +1003,16 @@ export function registerSPMTools(server: XcodeServer) {
               throw new XcodeServerError("No Package.swift found in the active directory. This project doesn't use Swift Package Manager.");
             }
     
    -        // Build the command with all options
    -        let args = `--configuration ${configuration}`;
    -
    -        if (target) {
    -          args += ` --target "${target}"`;
    -        }
    -
    -        if (product) {
    -          args += ` --product "${product}"`;
    -        }
    -
    -        if (showBinPath) {
    -          args += ` --show-bin-path`;
    -        }
    -
    -        if (buildTests) {
    -          args += ` --build-tests`;
    -        }
    -
    -        if (jobs) {
    -          args += ` --jobs ${jobs}`;
    -        }
    -
    -        if (verbose) {
    -          args += ` --verbose`;
    -        }
    +        const buildArgs = ["build", "--configuration", configuration];
    +        if (target) buildArgs.push("--target", target);
    +        if (product) buildArgs.push("--product", product);
    +        if (showBinPath) buildArgs.push("--show-bin-path");
    +        if (buildTests) buildArgs.push("--build-tests");
    +        if (jobs) buildArgs.push("--jobs", String(jobs));
    +        if (verbose) buildArgs.push("--verbose");
     
             try {
    -          const { stdout, stderr } = await execAsync(`swift build ${args}`, { cwd: activeDirectory });
    +          const { stdout, stderr } = await runExecFile("swift", buildArgs, { cwd: activeDirectory });
     
               // If binary path was requested, highlight it in the output
               let formattedOutput = stdout;
    @@ -1156,42 +1078,22 @@ export function registerSPMTools(server: XcodeServer) {
               throw new XcodeServerError("No Package.swift found in the active directory. This project doesn't use Swift Package Manager.");
             }
     
    -        // Build the command with all options
    -        let args = `--configuration ${configuration}`;
    -
    -        // If list tests is requested, we use a different subcommand
    -        if (listTests) {
    -          args = `list ${args}`;
    -        }
    -
    -        if (filter) {
    -          args += ` --filter "${filter}"`;
    -        }
    -
    -        if (skip) {
    -          args += ` --skip "${skip}"`;
    -        }
    -
    +        const testArgs = listTests ? ["test", "list", "--configuration", configuration] : ["test", "--configuration", configuration];
    +        if (filter) testArgs.push("--filter", filter);
    +        if (skip) testArgs.push("--skip", skip);
             if (parallel) {
    -          args += ` --parallel`;
    -
    -          if (numWorkers) {
    -            args += ` --num-workers ${numWorkers}`;
    -          }
    -        }
    -
    -        if (codeCoverage) {
    -          args += ` --enable-code-coverage`;
    +          testArgs.push("--parallel");
    +          if (numWorkers) testArgs.push("--num-workers", String(numWorkers));
             }
    -
    +        if (codeCoverage) testArgs.push("--enable-code-coverage");
             if (outputPath) {
               const resolvedOutputPath = server.pathManager.normalizePath(outputPath);
               server.pathManager.validatePathForWriting(resolvedOutputPath);
    -          args += ` --xunit-output "${resolvedOutputPath}"`;
    +          testArgs.push("--xunit-output", resolvedOutputPath);
             }
     
             try {
    -          const { stdout, stderr } = await execAsync(`swift test ${args}`, { cwd: activeDirectory });
    +          const { stdout, stderr } = await runExecFile("swift", testArgs, { cwd: activeDirectory });
     
               return {
                 content: [{
    @@ -1294,24 +1196,17 @@ export function registerSPMTools(server: XcodeServer) {
               throw new XcodeServerError("No Package.swift found in the active directory. This project doesn't use Swift Package Manager.");
             }
     
    -        // Build the command
    -        let args = `show-dependencies --format ${format}`;
    -
    -        if (verbose) {
    -          args += ` --verbose`;
    -        }
    -
    +        const showArgs = ["package", "show-dependencies", "--format", format];
    +        if (verbose) showArgs.push("--verbose");
             if (outputPath) {
               const resolvedOutputPath = server.pathManager.normalizePath(outputPath);
               server.pathManager.validatePathForWriting(resolvedOutputPath);
    -          args += ` --output-path "${resolvedOutputPath}"`;
    -
    -          // Ensure the output directory exists
    +          showArgs.push("--output-path", resolvedOutputPath);
               await fs.mkdir(path.dirname(resolvedOutputPath), { recursive: true });
             }
     
             try {
    -          const { stdout, stderr } = await execAsync(`swift package ${args}`, { cwd: activeDirectory });
    +          const { stdout, stderr } = await runExecFile("swift", showArgs, { cwd: activeDirectory });
     
               // Try to get information about the Package.swift dependencies directly
               let packageDepsInfo = '';
    @@ -1390,7 +1285,7 @@ export function registerSPMTools(server: XcodeServer) {
             }
     
             try {
    -          const { stdout, stderr } = await execAsync(`swift package ${command}`, { cwd: activeDirectory });
    +          const { stdout, stderr } = await runExecFile("swift", ["package", command], { cwd: activeDirectory });
     
               return {
                 content: [{
    @@ -1442,7 +1337,7 @@ export function registerSPMTools(server: XcodeServer) {
             }
     
             try {
    -          const { stdout, stderr } = await execAsync(`swift package dump-package`, { cwd: activeDirectory });
    +          const { stdout, stderr } = await runExecFile("swift", ["package", "dump-package"], { cwd: activeDirectory });
     
               // Try to parse and pretty print the JSON
               let formattedOutput = stdout;
    @@ -1517,31 +1412,18 @@ export function registerSPMTools(server: XcodeServer) {
             try {
               // First, check if swift-docc is available
               try {
    -            await execAsync('which swift-docc');
    +            await runExecFile("which", ["swift-docc"]);
               } catch {
                 throw new Error(
                   "swift-docc command not found. DocC is available in Swift 5.5 and later. " +
                   "Make sure you have the latest version of Swift installed."
                 );
               }
     
    -          // Build the command with all options
    -          let cmd = `cd "${activeDirectory}" && swift package --disable-sandbox generate-documentation`;
    -
    -          // Add output path
    -          cmd += ` --output-path "${resolvedOutputPath}"`;
    -
    -          // Add hosting base path if provided
    -          if (hostingBasePath) {
    -            cmd += ` --hosting-base-path "${hostingBasePath}"`;
    -          }
    -
    -          // Add transform for static hosting if requested
    -          if (transformForStaticHosting) {
    -            cmd += ` --transform-for-static-hosting`;
    -          }
    -
    -          const { stdout, stderr } = await execAsync(cmd);
    +          const docArgs = ["package", "--disable-sandbox", "generate-documentation", "--output-path", resolvedOutputPath];
    +          if (hostingBasePath) docArgs.push("--hosting-base-path", hostingBasePath);
    +          if (transformForStaticHosting) docArgs.push("--transform-for-static-hosting");
    +          const { stdout, stderr } = await runExecFile("swift", docArgs, { cwd: activeDirectory });
     
               // Determine the index.html path for opening in browser
               const indexPath = path.join(resolvedOutputPath, 'index.html');
    @@ -1553,9 +1435,7 @@ export function registerSPMTools(server: XcodeServer) {
                   // Check if the index.html file exists
                   await fs.access(indexPath);
     
    -              // Open in browser
    -              const { exec } = await import('child_process');
    -              exec(`open "${indexPath}"`);
    +              await runExecFile("open", [indexPath]);
     
                   browserMessage = `\n\nDocumentation opened in your default browser.`;
                 } catch {
    
  • src/tools/xcode/index.ts+78 105 modified
    @@ -1,34 +1,36 @@
     import { z } from "zod";
    -import { promisify } from "util";
    -import { exec } from "child_process";
     import * as path from "path";
     import * as fs from "fs/promises";
     import { XcodeServer } from "../../server.js";
     import { CommandExecutionError, PathAccessError, FileOperationError } from "../../utils/errors.js";
    -
    -const execAsync = promisify(exec);
    +import { runExecFile } from "../../utils/execFile.js";
     
     /**
      * Get available Xcode versions on the system
      */
     async function getXcodeVersions(): Promise<{path: string, version: string, build: string, isDefault: boolean}[]> {
       try {
    -    const { stdout } = await execAsync('mdfind "kMDItemCFBundleIdentifier == com.apple.dt.Xcode"');
    -    const paths = stdout.trim().split('\n').filter(Boolean);
    +    const { stdout } = await runExecFile("mdfind", ["kMDItemCFBundleIdentifier == com.apple.dt.Xcode"]);
    +    const paths = stdout.trim().split("\n").filter(Boolean);
     
         const versions = [];
         const defaultPath = await getDefaultXcodePath();
     
    -    for (const path of paths) {
    +    for (const p of paths) {
           try {
    -        const { stdout: versionOutput } = await execAsync(`/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "${path}/Contents/Info.plist"`);
    -        const { stdout: buildOutput } = await execAsync(`/usr/libexec/PlistBuddy -c "Print :DTXcode" "${path}/Contents/Info.plist"`);
    +        const plistPath = path.join(p, "Contents", "Info.plist");
    +        const { stdout: versionOutput } = await runExecFile("/usr/libexec/PlistBuddy", [
    +          "-c", "Print :CFBundleShortVersionString", plistPath
    +        ]);
    +        const { stdout: buildOutput } = await runExecFile("/usr/libexec/PlistBuddy", [
    +          "-c", "Print :DTXcode", plistPath
    +        ]);
     
             versions.push({
    -          path,
    +          path: p,
               version: versionOutput.trim(),
               build: buildOutput.trim(),
    -          isDefault: path === defaultPath
    +          isDefault: p === defaultPath
             });
           } catch {
             // Skip if we can't get version info
    @@ -47,7 +49,7 @@ async function getXcodeVersions(): Promise<{path: string, version: string, build
      */
     async function getDefaultXcodePath(): Promise<string> {
       try {
    -    const { stdout } = await execAsync('xcode-select -p');
    +    const { stdout } = await runExecFile("xcode-select", ["-p"]);
         // xcode-select returns the Developer directory, we need to go up two levels
         return path.dirname(path.dirname(stdout.trim()));
       } catch (error) {
    @@ -71,36 +73,30 @@ export function registerXcodeTools(server: XcodeServer) {
         },
         async ({ tool, args = "", workingDir }) => {
           try {
    -        let options = {};
    -
    +        const options: { cwd?: string } = {};
             if (workingDir) {
    -          // Validate and resolve the working directory path
               const resolvedWorkingDir = server.pathManager.normalizePath(workingDir);
               server.pathManager.validatePathForReading(resolvedWorkingDir);
    -          options = { cwd: resolvedWorkingDir };
    +          options.cwd = resolvedWorkingDir;
             }
     
    -        const { stdout, stderr } = await execAsync(`xcrun ${tool} ${args}`, options);
    +        const xcrunArgs = args ? [tool, args] : [tool];
    +        const { stdout, stderr } = await runExecFile("xcrun", xcrunArgs, options);
     
    -      return {
    -        content: [{
    +        return {
    +          content: [{
                 type: "text",
    -            text: `${tool} output:\n${stdout}\n${stderr ? 'Error output:\n' + stderr : ''}`
    +            text: `${tool} output:\n${stdout}\n${stderr ? "Error output:\n" + stderr : ""}`
               }]
             };
           } catch (error) {
    -        let stderr = '';
    -        if (error instanceof Error && 'stderr' in error) {
    -          stderr = (error as any).stderr;
    -        }
    -
             if (error instanceof PathAccessError) {
               throw new Error(`Access denied: ${error.message}`);
             }
    -
    +        if (error instanceof CommandExecutionError) throw error;
             throw new CommandExecutionError(
               `xcrun ${tool}`,
    -          stderr || (error instanceof Error ? error.message : String(error))
    +          error instanceof Error ? error.message : String(error)
             );
           }
         }
    @@ -149,30 +145,26 @@ export function registerXcodeTools(server: XcodeServer) {
               throw new FileOperationError(`Failed to create output directory: ${outputDir}`, String(error));
             }
     
    -        // Build the command with validated paths
    -        let cmd = `xcrun actool "${resolvedCatalogPath}" --output-format human-readable-text --notices --warnings`;
    -
    -        // Add platform
    -        cmd += ` --platform ${platform}`;
    -
    -        // Add deployment target
    -        cmd += ` --minimum-deployment-target ${minDeploymentTarget}`;
    -
    -        // Add target devices
    +        const plistPath = path.join(resolvedOutputDir, "assetcatalog_generated_info.plist");
    +        const actoolArgs = [
    +          resolvedCatalogPath,
    +          "--output-format", "human-readable-text",
    +          "--notices", "--warnings",
    +          "--platform", platform,
    +          "--minimum-deployment-target", minDeploymentTarget,
    +        ];
             for (const device of targetDevices) {
    -          cmd += ` --target-device ${device}`;
    +          actoolArgs.push("--target-device", device);
             }
    -
    -        // Add app icon if specified
             if (appIcon) {
    -          cmd += ` --app-icon ${appIcon}`;
    +          actoolArgs.push("--app-icon", appIcon);
             }
    +        actoolArgs.push(
    +          "--output-partial-info-plist", plistPath,
    +          "--compress-pngs", "--compile", resolvedOutputDir
    +        );
     
    -        // Add output paths
    -        const plistPath = path.join(resolvedOutputDir, 'assetcatalog_generated_info.plist');
    -        cmd += ` --output-partial-info-plist "${plistPath}" --compress-pngs --compile "${resolvedOutputDir}"`;
    -
    -        const { stdout, stderr } = await execAsync(cmd);
    +        const { stdout, stderr } = await runExecFile("xcrun", ["actool", ...actoolArgs]);
     
           return {
             content: [{
    @@ -209,29 +201,24 @@ export function registerXcodeTools(server: XcodeServer) {
         },
         async ({ args = "", command }) => {
           try {
    -        let cmd = `xcrun lldb ${args}`;
    -
    -        // If a single command is provided, execute it and exit
    +        const lldbArgs: string[] = ["lldb"];
    +        if (args) lldbArgs.push(args);
             if (command) {
    -          cmd += ` -o "${command}" -b`;  // -b for batch mode (exit after running commands)
    +          lldbArgs.push("-o", command, "-b");
             }
    +        const { stdout, stderr } = await runExecFile("xcrun", lldbArgs);
     
    -        const { stdout, stderr } = await execAsync(cmd);
    -
    -      return {
    -        content: [{
    +        return {
    +          content: [{
                 type: "text",
    -            text: `LLDB output:\n${stdout}\n${stderr ? 'Error output:\n' + stderr : ''}`
    +            text: `LLDB output:\n${stdout}\n${stderr ? "Error output:\n" + stderr : ""}`
               }]
             };
           } catch (error) {
    -        let stderr = '';
    -        if (error instanceof Error && 'stderr' in error) {
    -          stderr = (error as any).stderr;
    -        }
    +        if (error instanceof CommandExecutionError) throw error;
             throw new CommandExecutionError(
    -          'xcrun lldb',
    -          stderr || (error instanceof Error ? error.message : String(error))
    +          "xcrun lldb",
    +          error instanceof Error ? error.message : String(error)
             );
           }
         }
    @@ -267,16 +254,16 @@ export function registerXcodeTools(server: XcodeServer) {
             const outputDir = path.dirname(resolvedOutputPath);
             await fs.mkdir(outputDir, { recursive: true });
     
    -        // Build the command
    -        let cmd = `xcrun xctrace record --template "${template}" --time-limit ${duration}s --output "${resolvedOutputPath}"`;
    +        const xctraceArgs = [
    +          "xctrace", "record",
    +          "--template", template,
    +          "--time-limit", `${duration}s`,
    +          "--output", resolvedOutputPath,
    +        ];
    +        if (startSuspended) xctraceArgs.push("--launch-suspended");
    +        xctraceArgs.push("--launch", "--", resolvedAppPath);
     
    -        if (startSuspended) {
    -          cmd += ' --launch-suspended';
    -        }
    -
    -        cmd += ` --launch -- "${resolvedAppPath}"`;
    -
    -        const { stdout, stderr } = await execAsync(cmd);
    +        const { stdout, stderr } = await runExecFile("xcrun", xctraceArgs);
     
             return {
               content: [{
    @@ -327,41 +314,33 @@ export function registerXcodeTools(server: XcodeServer) {
     
             // Add SDK information
             try {
    -          // Get iOS SDK information
    -          const { stdout: iOSSDKOutput } = await execAsync('xcrun --show-sdk-path --sdk iphoneos');
    +          const { stdout: iOSSDKOutput } = await runExecFile("xcrun", ["--show-sdk-path", "--sdk", "iphoneos"]);
               formattedOutput += `\niOS SDK Path: ${iOSSDKOutput.trim()}\n`;
    -
    -          const { stdout: iOSSDKVersionOutput } = await execAsync('xcrun --show-sdk-version --sdk iphoneos');
    +          const { stdout: iOSSDKVersionOutput } = await runExecFile("xcrun", ["--show-sdk-version", "--sdk", "iphoneos"]);
               formattedOutput += `iOS SDK Version: ${iOSSDKVersionOutput.trim()}\n`;
     
    -          // Get macOS SDK information
               try {
    -            const { stdout: macOSSDKOutput } = await execAsync('xcrun --show-sdk-path --sdk macosx');
    +            const { stdout: macOSSDKOutput } = await runExecFile("xcrun", ["--show-sdk-path", "--sdk", "macosx"]);
                 formattedOutput += `\nmacOS SDK Path: ${macOSSDKOutput.trim()}\n`;
    -
    -            const { stdout: macOSSDKVersionOutput } = await execAsync('xcrun --show-sdk-version --sdk macosx');
    +            const { stdout: macOSSDKVersionOutput } = await runExecFile("xcrun", ["--show-sdk-version", "--sdk", "macosx"]);
                 formattedOutput += `macOS SDK Version: ${macOSSDKVersionOutput.trim()}\n`;
               } catch {
                 // Ignore if macOS SDK info can't be retrieved
               }
     
    -          // Get watchOS SDK information
               try {
    -            const { stdout: watchOSSDKOutput } = await execAsync('xcrun --show-sdk-path --sdk watchos');
    +            const { stdout: watchOSSDKOutput } = await runExecFile("xcrun", ["--show-sdk-path", "--sdk", "watchos"]);
                 formattedOutput += `\nwatchOS SDK Path: ${watchOSSDKOutput.trim()}\n`;
    -
    -            const { stdout: watchOSSDKVersionOutput } = await execAsync('xcrun --show-sdk-version --sdk watchos');
    +            const { stdout: watchOSSDKVersionOutput } = await runExecFile("xcrun", ["--show-sdk-version", "--sdk", "watchos"]);
                 formattedOutput += `watchOS SDK Version: ${watchOSSDKVersionOutput.trim()}\n`;
               } catch {
                 // Ignore if watchOS SDK info can't be retrieved
               }
     
    -          // Get tvOS SDK information
               try {
    -            const { stdout: tvOSSDKOutput } = await execAsync('xcrun --show-sdk-path --sdk appletvos');
    +            const { stdout: tvOSSDKOutput } = await runExecFile("xcrun", ["--show-sdk-path", "--sdk", "appletvos"]);
                 formattedOutput += `\ntvOS SDK Path: ${tvOSSDKOutput.trim()}\n`;
    -
    -            const { stdout: tvOSSDKVersionOutput } = await execAsync('xcrun --show-sdk-version --sdk appletvos');
    +            const { stdout: tvOSSDKVersionOutput } = await runExecFile("xcrun", ["--show-sdk-version", "--sdk", "appletvos"]);
                 formattedOutput += `tvOS SDK Version: ${tvOSSDKVersionOutput.trim()}\n`;
               } catch {
                 // Ignore if tvOS SDK info can't be retrieved
    @@ -460,10 +439,8 @@ export function registerXcodeTools(server: XcodeServer) {
               throw new Error('No target Xcode version found');
             }
     
    -        // Use the path module
    -        // (already imported at the top of the file)
    -        const developerDir = path.join(targetXcode.path, 'Contents', 'Developer');
    -        const { stdout, stderr } = await execAsync(`sudo xcode-select --switch "${developerDir}"`);
    +        const developerDir = path.join(targetXcode.path, "Contents", "Developer");
    +        const { stdout, stderr } = await runExecFile("xcode-select", ["--switch", developerDir]);
     
             return {
               content: [{
    @@ -595,9 +572,12 @@ export function registerXcodeTools(server: XcodeServer) {
             // Write the export options plist
             await fs.writeFile(exportOptionsPath, exportOptionsContent, 'utf-8');
     
    -        // Run the export command
    -        const cmd = `xcrun xcodebuild -exportArchive -archivePath "${resolvedArchivePath}" -exportOptionsPlist "${exportOptionsPath}" -exportPath "${resolvedExportPath}"`;
    -        const { stdout, stderr } = await execAsync(cmd);
    +        const { stdout, stderr } = await runExecFile("xcrun", [
    +          "xcodebuild", "-exportArchive",
    +          "-archivePath", resolvedArchivePath,
    +          "-exportOptionsPlist", exportOptionsPath,
    +          "-exportPath", resolvedExportPath
    +        ]);
     
             // Clean up the temporary directory
             try {
    @@ -705,19 +685,14 @@ export function registerXcodeTools(server: XcodeServer) {
               }
             }
     
    -        // Build the validation command
    -        let cmd;
    -
    +        const altoolArgs = ["altool", "--validate-app", "-f", resolvedIpaPath, "--type", "ios"];
             if (apiKey && apiIssuer && apiKeyPath) {
    -          // Use API Key authentication
               const resolvedApiKeyPath = server.pathManager.normalizePath(apiKeyPath);
    -          cmd = `xcrun altool --validate-app -f "${resolvedIpaPath}" --apiKey "${apiKey}" --apiIssuer "${apiIssuer}" --api-key-path "${resolvedApiKeyPath}" --type ios`;
    +          altoolArgs.push("--apiKey", apiKey, "--apiIssuer", apiIssuer, "--api-key-path", resolvedApiKeyPath);
             } else {
    -          // Use username/password authentication
    -          cmd = `xcrun altool --validate-app -f "${resolvedIpaPath}" -u "${username}" -p "${password}" --type ios`;
    +          altoolArgs.push("-u", username, "-p", password);
             }
    -
    -        const { stdout, stderr } = await execAsync(cmd);
    +        const { stdout, stderr } = await runExecFile("xcrun", altoolArgs);
     
             // Check for validation success
             if (stdout.includes("No errors validating")) {
    @@ -844,9 +819,7 @@ export function registerXcodeTools(server: XcodeServer) {
                 const filename = `icon_${size}x${size}@${scale}x.png`;
                 const outputFilePath = path.join(iconsetPath, filename);
     
    -            // Generate the resized image
    -            const resizeCmd = `sips -Z ${pixelSize} "${resolvedSourceImage}" --out "${outputFilePath}"`;
    -            await execAsync(resizeCmd);
    +            await runExecFile("sips", ["-Z", String(pixelSize), resolvedSourceImage, "--out", outputFilePath]);
     
                 // Add to Contents.json
                 (contentsJson.images as any[]).push({
    
  • src/utils/execFile.ts+45 0 added
    @@ -0,0 +1,45 @@
    +import { execFile } from "child_process";
    +import { promisify } from "util";
    +import { CommandExecutionError } from "./errors.js";
    +
    +const execFileAsync = promisify(execFile);
    +
    +export interface RunExecFileOptions {
    +  cwd?: string;
    +  env?: NodeJS.ProcessEnv;
    +  timeout?: number;
    +  maxBuffer?: number;
    +}
    +
    +/**
    + * Run a binary with argument array. No shell is invoked; arguments are passed
    + * literally to the process. Use this instead of exec() to prevent CWE-78
    + * command injection when any argument is or could be user-controlled.
    + */
    +export async function runExecFile(
    +  file: string,
    +  args: string[],
    +  options?: RunExecFileOptions
    +): Promise<{ stdout: string; stderr: string }> {
    +  try {
    +    const result = await execFileAsync(file, args, {
    +      encoding: "utf8",
    +      ...options,
    +    });
    +    return {
    +      stdout: result.stdout ?? "",
    +      stderr: result.stderr ?? "",
    +    };
    +  } catch (error) {
    +    const stderr =
    +      error && typeof error === "object" && "stderr" in error
    +        ? String((error as { stderr: string }).stderr)
    +        : "";
    +    const message =
    +      error instanceof Error ? error.message : String(error);
    +    throw new CommandExecutionError(
    +      [file, ...args].join(" "),
    +      stderr || message
    +    );
    +  }
    +}
    
  • src/utils/project.ts+33 53 modified
    @@ -1,22 +1,17 @@
    -import * as path from 'path';
    -import * as fs from 'fs/promises';
    -import { promisify } from 'util';
    -import { exec, execFile } from 'child_process';
    -import { isInXcodeProject } from './file.js';
    -import { XcodeProject, ProjectInfo } from '../types/index.js';
    -
    -const execAsync = promisify(exec);
    -const execFileAsync = promisify(execFile);
    +import * as path from "path";
    +import * as fs from "fs/promises";
    +import { isInXcodeProject } from "./file.js";
    +import { XcodeProject, ProjectInfo } from "../types/index.js";
    +import { runExecFile } from "./execFile.js";
     
     /**
      * Find all Xcode projects in the given search path
      */
     export async function findXcodeProjects(searchPath = "."): Promise<XcodeProject[]> {
       try {
    -    // Find .xcodeproj, .xcworkspace, and Package.swift files
    -    const { stdout: projStdout } = await execFileAsync('find', [searchPath, '-name', '*.xcodeproj']);
    -    const { stdout: workspaceStdout } = await execFileAsync('find', [searchPath, '-name', '*.xcworkspace']);
    -    const { stdout: spmStdout } = await execFileAsync('find', [searchPath, '-name', 'Package.swift']);
    +    const { stdout: projStdout } = await runExecFile("find", [searchPath, "-name", "*.xcodeproj"]);
    +    const { stdout: workspaceStdout } = await runExecFile("find", [searchPath, "-name", "*.xcworkspace"]);
    +    const { stdout: spmStdout } = await runExecFile("find", [searchPath, "-name", "Package.swift"]);
     
         const projects: XcodeProject[] = [];
     
    @@ -88,8 +83,8 @@ export async function findXcodeProjects(searchPath = "."): Promise<XcodeProject[
      */
     export async function isProjectInWorkspace(projectPath: string): Promise<boolean> {
       const projectDir = path.dirname(projectPath);
    -  const workspaceCheck = await execFileAsync('find', [projectDir, '-maxdepth', '2', '-name', '*.xcworkspace']);
    -  return workspaceCheck.stdout.trim().length > 0;
    +  const { stdout } = await runExecFile("find", [projectDir, "-maxdepth", "2", "-name", "*.xcworkspace"]);
    +  return stdout.trim().length > 0;
     }
     
     /**
    @@ -146,39 +141,31 @@ export async function findMainProjectInWorkspace(workspacePath: string): Promise
      */
     export async function getProjectInfo(projectPath: string): Promise<ProjectInfo> {
       try {
    -    // Determine the right command based on the project path type
    -    let cmd: string;
    -
    -    if (projectPath.endsWith('.xcworkspace')) {
    -      // For workspaces, use -workspace flag
    -      cmd = `xcodebuild -list -workspace "${projectPath}"`;
    -    } else if (projectPath.endsWith('/project.xcworkspace')) {
    -      // Handle the case where we incorrectly get a project.xcworkspace inside an .xcodeproj
    -      // Strip off the /project.xcworkspace and use the .xcodeproj with -project flag
    -      const xcodeProjectPath = projectPath.replace('/project.xcworkspace', '');
    -      cmd = `xcodebuild -list -project "${xcodeProjectPath}"`;
    -    } else if (projectPath.endsWith('.xcodeproj')) {
    -      // Standard project
    -      cmd = `xcodebuild -list -project "${projectPath}"`;
    +    let args: string[];
    +
    +    if (projectPath.endsWith(".xcworkspace")) {
    +      args = ["-list", "-workspace", projectPath];
    +    } else if (projectPath.endsWith("/project.xcworkspace")) {
    +      const xcodeProjectPath = projectPath.replace("/project.xcworkspace", "");
    +      args = ["-list", "-project", xcodeProjectPath];
    +    } else if (projectPath.endsWith(".xcodeproj")) {
    +      args = ["-list", "-project", projectPath];
         } else {
    -      // Check if it's an SPM project
    -      const packageSwiftPath = path.join(projectPath, 'Package.swift');
    +      const packageSwiftPath = path.join(projectPath, "Package.swift");
           try {
             await fs.access(packageSwiftPath);
    -        // For SPM projects, return basic info
             return {
               path: projectPath,
    -          targets: ['all'],
    -          configurations: ['debug', 'release'],
    -          schemes: ['all']
    +          targets: ["all"],
    +          configurations: ["debug", "release"],
    +          schemes: ["all"]
             };
           } catch {
    -        // Not an SPM project, try as a standard project
    -        cmd = `xcodebuild -list -project "${projectPath}"`;
    +        args = ["-list", "-project", projectPath];
           }
         }
     
    -    const { stdout } = await execAsync(cmd);
    +    const { stdout } = await runExecFile("xcodebuild", args);
         const info: ProjectInfo = {
           path: projectPath,
           targets: [],
    @@ -211,23 +198,16 @@ export async function getProjectInfo(projectPath: string): Promise<ProjectInfo>
      */
     export async function getWorkspaceInfo(workspacePath: string): Promise<ProjectInfo> {
       try {
    -    // Handle different path formats
    -    let cmd: string;
    -
    -    if (workspacePath.endsWith('.xcworkspace')) {
    -      // Standard workspace
    -      cmd = `xcodebuild -workspace "${workspacePath}" -list`;
    -    } else if (workspacePath.endsWith('/project.xcworkspace')) {
    -      // Handle case where we get project.xcworkspace inside an .xcodeproj
    -      const xcodeProjectPath = workspacePath.replace('/project.xcworkspace', '');
    -      // In this case, use project instead of workspace
    -      cmd = `xcodebuild -project "${xcodeProjectPath}" -list`;
    +    let args: string[];
    +    if (workspacePath.endsWith(".xcworkspace")) {
    +      args = ["-workspace", workspacePath, "-list"];
    +    } else if (workspacePath.endsWith("/project.xcworkspace")) {
    +      const xcodeProjectPath = workspacePath.replace("/project.xcworkspace", "");
    +      args = ["-project", xcodeProjectPath, "-list"];
         } else {
    -      // Default to treating it as a workspace
    -      cmd = `xcodebuild -workspace "${workspacePath}" -list`;
    +      args = ["-workspace", workspacePath, "-list"];
         }
    -
    -    const { stdout } = await execAsync(cmd);
    +    const { stdout } = await runExecFile("xcodebuild", args);
         const info: ProjectInfo = {
           path: workspacePath,
           targets: [],
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

8

News mentions

0

No linked articles in our index yet.