VYPR
High severity7.5OSV Advisory· Published Jul 1, 2025· Updated Apr 15, 2026

CVE-2025-53107

CVE-2025-53107

Description

@cyanheads/git-mcp-server is an MCP server designed to interact with Git repositories. Prior to version 2.1.5, there is a command injection vulnerability caused by the unsanitized use of input parameters within a call to child_process.exec, enabling an attacker to inject arbitrary system commands. Successful exploitation can lead to remote code execution under the server process's privileges. The server constructs and executes shell commands using unvalidated user input directly within command-line strings. This introduces the possibility of shell metacharacter injection (|, >, &&, etc.). An MCP Client can be instructed to execute additional actions for example via indirect prompt injection when asked to read git logs. This issue has been patched in version 2.1.5.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
@cyanheads/git-mcp-servernpm
< 2.1.52.1.5

Affected products

1

Patches

1
0dbd6995ccdf

fix(security): patch command injection vulnerability

https://github.com/cyanheads/git-mcp-servercyanheadsJun 29, 2025via ghsa
26 files changed · +519 2771
  • CHANGELOG.md+5 0 modified
    @@ -2,6 +2,11 @@
     
     All notable changes to this project will be documented in this file.
     
    +## v2.1.5 - 2025-06-29
    +
    +### Security
    +- Patched a command injection vulnerability where unsanitized user input could be passed to `child_process.exec`. All `exec` calls have been replaced with the safer `execFile` method, which treats arguments as distinct values rather than executable script parts. Thank you to [@dellalibera](https://github.com/dellalibera) for the disclosure. For more details, see the security advisory: [GHSA-3q26-f695-pp76](https://github.com/cyanheads/git-mcp-server/security/advisories/GHSA-3q26-f695-pp76).
    +
     ## v2.1.4 - 2025-06-20
     
     ### Changed
    
  • package.json+2 13 modified
    @@ -1,6 +1,6 @@
     {
       "name": "@cyanheads/git-mcp-server",
    -  "version": "2.1.4",
    +  "version": "2.1.5",
       "description": "An MCP (Model Context Protocol) server enabling LLMs and AI agents to interact with Git repositories. Provides tools for comprehensive Git operations including clone, commit, branch, diff, log, status, push, pull, merge, rebase, worktree, tag management, and more, via the MCP standard. STDIO & HTTP.",
       "main": "dist/index.js",
       "files": [
    @@ -36,21 +36,14 @@
       },
       "dependencies": {
         "@hono/node-server": "^1.14.4",
    -    "@modelcontextprotocol/inspector": "^0.14.3",
         "@modelcontextprotocol/sdk": "^1.13.0",
    -    "@types/jsonwebtoken": "^9.0.10",
         "@types/node": "^24.0.3",
         "@types/sanitize-html": "^2.16.0",
         "@types/validator": "^13.15.2",
    -    "chalk": "^5.4.1",
         "chrono-node": "2.8.0",
    -    "cli-table3": "^0.6.5",
         "dotenv": "^16.5.0",
    -    "express": "^5.1.0",
         "hono": "^4.8.2",
    -    "ignore": "^7.0.5",
         "jose": "^6.0.11",
    -    "jsonwebtoken": "^9.0.2",
         "openai": "^5.6.0",
         "partial-json": "^0.1.7",
         "sanitize-html": "^2.17.0",
    @@ -59,8 +52,7 @@
         "typescript": "^5.8.3",
         "validator": "^13.15.15",
         "winston": "^3.17.0",
    -    "winston-daily-rotate-file": "^5.0.0",
    -    "yargs": "^18.0.0",
    +    "winston-transport": "^4.7.0",
         "zod": "^3.25.67"
       },
       "keywords": [
    @@ -113,9 +105,6 @@
         "node": ">=20.0.0"
       },
       "devDependencies": {
    -    "@types/express": "^5.0.3",
    -    "@types/js-yaml": "^4.0.9",
    -    "js-yaml": "^4.1.0",
         "prettier": "^3.5.3",
         "typedoc": "^0.28.5"
       },
    
  • package-lock.json+48 2314 modified
  • src/mcp-server/tools/gitAdd/logic.ts+20 58 modified
    @@ -1,4 +1,4 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import { promisify } from "util";
     import { z } from "zod";
     // Import utils from barrel (logger from ../utils/internal/logger.js)
    @@ -9,7 +9,7 @@ import { RequestContext } from "../../../utils/index.js";
     // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
     import { sanitization } from "../../../utils/index.js";
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the input schema for the git_add tool using Zod
     export const GitAddInputSchema = z.object({
    @@ -107,66 +107,28 @@ export async function addGitFiles(
         );
       }
     
    -  // Prepare the files argument for the command, ensuring proper quoting
    -  let filesArg: string;
    -  const filesToStage = input.files; // Keep original for reporting
    +  const filesToStage = Array.isArray(input.files)
    +    ? input.files
    +    : [input.files];
    +  if (filesToStage.length === 0) {
    +    filesToStage.push("."); // Default to staging all if array is empty
    +  }
    +
       try {
    -    if (Array.isArray(filesToStage)) {
    -      if (filesToStage.length === 0) {
    -        logger.warning(
    -          "Empty array provided for files, defaulting to staging all changes.",
    -          { ...context, operation },
    -        );
    -        filesArg = "."; // Default to staging all if array is empty
    -      } else {
    -        // Quote each file path individually
    -        filesArg = filesToStage
    -          .map((file) => {
    -            const sanitizedFile = file.startsWith("-") ? `./${file}` : file; // Prefix with './' if it starts with a dash
    -            return `"${sanitizedFile.replace(/"/g, '\\"')}"`; // Escape quotes within path
    -          })
    -          .join(" ");
    -      }
    -    } else {
    -      // Single string case
    -      const sanitizedFile = filesToStage.startsWith("-")
    -        ? `./${filesToStage}`
    -        : filesToStage; // Prefix with './' if it starts with a dash
    -      filesArg = `"${sanitizedFile.replace(/"/g, '\\"')}"`;
    -    }
    -  } catch (err) {
    -    logger.error("File path validation/quoting failed", {
    +    const args = ["-C", targetPath, "add", "--"];
    +    filesToStage.forEach((file) => {
    +      // Sanitize each file path. Although execFile is safer,
    +      // this prevents arguments like "-v" from being treated as flags by git.
    +      const sanitizedFile = file.startsWith("-") ? `./${file}` : file;
    +      args.push(sanitizedFile);
    +    });
    +
    +    logger.debug(`Executing command: git ${args.join(" ")}`, {
           ...context,
           operation,
    -      files: filesToStage,
    -      error: err,
         });
    -    throw new McpError(
    -      BaseErrorCode.VALIDATION_ERROR,
    -      `Invalid file path/pattern provided: ${err instanceof Error ? err.message : String(err)}`,
    -      { context, operation, originalError: err },
    -    );
    -  }
    -
    -  // This check should ideally not be needed now due to the logic above
    -  if (!filesArg) {
    -    logger.error(
    -      "Internal error: filesArg is unexpectedly empty after processing.",
    -      { ...context, operation },
    -    );
    -    throw new McpError(
    -      BaseErrorCode.INTERNAL_ERROR,
    -      "Internal error preparing git add command.",
    -      { context, operation },
    -    );
    -  }
    -
    -  try {
    -    // Use the resolved targetPath
    -    const command = `git -C "${targetPath}" add -- ${filesArg}`;
    -    logger.debug(`Executing command: ${command}`, { ...context, operation });
     
    -    const { stdout, stderr } = await execAsync(command);
    +    const { stdout, stderr } = await execFileAsync("git", args);
     
         if (stderr) {
           // Log stderr as warning, as 'git add' can produce warnings but still succeed.
    @@ -216,7 +178,7 @@ export async function addGitFiles(
           // Still throw an error, but return structured info in the catch block of the registration
           throw new McpError(
             BaseErrorCode.NOT_FOUND,
    -        `Specified files/patterns did not match any files in ${targetPath}: ${filesArg}`,
    +        `Specified files/patterns did not match any files in ${targetPath}: ${filesToStage.join(", ")}`,
             { context, operation, originalError: error, filesStaged: filesToStage },
           );
         }
    
  • src/mcp-server/tools/gitBranch/logic.ts+38 30 modified
    @@ -1,4 +1,4 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import { promisify } from "util";
     import { z } from "zod";
     // Import utils from barrel (logger from ../utils/internal/logger.js)
    @@ -9,7 +9,7 @@ import { RequestContext } from "../../../utils/index.js";
     // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
     import { sanitization } from "../../../utils/index.js";
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the BASE input schema for the git_branch tool using Zod
     export const GitBranchBaseSchema = z.object({
    @@ -221,22 +221,24 @@ export async function gitBranchLogic(
       }
     
       try {
    -    let command: string;
    +    let args: string[];
         let result: GitBranchResult;
     
         switch (input.mode) {
           case "list":
    -        command = `git -C "${targetPath}" branch --list --no-color`; // Start with basic list
    -        if (input.all)
    -          command += " -a"; // Add -a if requested
    -        else if (input.remote) command += " -r"; // Add -r if requested (exclusive with -a)
    -        command += " --verbose"; // Add verbose for commit info
    +        args = ["-C", targetPath, "branch", "--list", "--no-color"]; // Start with basic list
    +        if (input.all) {
    +          args.push("-a"); // Add -a if requested
    +        } else if (input.remote) {
    +          args.push("-r"); // Add -r if requested (exclusive with -a)
    +        }
    +        args.push("--verbose"); // Add verbose for commit info
     
    -        logger.debug(`Executing command: ${command}`, {
    +        logger.debug(`Executing command: git ${args.join(" ")}`, {
               ...context,
               operation,
             });
    -        const { stdout: listStdout } = await execAsync(command);
    +        const { stdout: listStdout } = await execFileAsync("git", args);
     
             const branches: BranchInfo[] = listStdout
               .trim()
    @@ -269,16 +271,20 @@ export async function gitBranchLogic(
     
           case "create":
             // branchName is validated by Zod refine
    -        command = `git -C "${targetPath}" branch `;
    -        if (input.force) command += "-f ";
    -        command += `"${input.branchName!}"`; // branchName is guaranteed by refine
    -        if (input.startPoint) command += ` "${input.startPoint}"`;
    +        args = ["-C", targetPath, "branch"];
    +        if (input.force) {
    +          args.push("-f");
    +        }
    +        args.push(input.branchName!); // branchName is guaranteed by refine
    +        if (input.startPoint) {
    +          args.push(input.startPoint);
    +        }
     
    -        logger.debug(`Executing command: ${command}`, {
    +        logger.debug(`Executing command: git ${args.join(" ")}`, {
               ...context,
               operation,
             });
    -        await execAsync(command);
    +        await execFileAsync("git", args);
             result = {
               success: true,
               mode: "create",
    @@ -289,16 +295,18 @@ export async function gitBranchLogic(
     
           case "delete":
             // branchName is validated by Zod refine
    -        command = `git -C "${targetPath}" branch `;
    -        if (input.remote) command += "-r ";
    -        command += input.force ? "-D " : "-d ";
    -        command += `"${input.branchName!}"`; // branchName is guaranteed by refine
    +        args = ["-C", targetPath, "branch"];
    +        if (input.remote) {
    +          args.push("-r");
    +        }
    +        args.push(input.force ? "-D" : "-d");
    +        args.push(input.branchName!); // branchName is guaranteed by refine
     
    -        logger.debug(`Executing command: ${command}`, {
    +        logger.debug(`Executing command: git ${args.join(" ")}`, {
               ...context,
               operation,
             });
    -        const { stdout: deleteStdout } = await execAsync(command);
    +        const { stdout: deleteStdout } = await execFileAsync("git", args);
             result = {
               success: true,
               mode: "delete",
    @@ -312,15 +320,15 @@ export async function gitBranchLogic(
     
           case "rename":
             // branchName and newBranchName validated by Zod refine
    -        command = `git -C "${targetPath}" branch `;
    -        command += input.force ? "-M " : "-m ";
    -        command += `"${input.branchName!}" "${input.newBranchName!}"`;
    +        args = ["-C", targetPath, "branch"];
    +        args.push(input.force ? "-M" : "-m");
    +        args.push(input.branchName!, input.newBranchName!);
     
    -        logger.debug(`Executing command: ${command}`, {
    +        logger.debug(`Executing command: git ${args.join(" ")}`, {
               ...context,
               operation,
             });
    -        await execAsync(command);
    +        await execFileAsync("git", args);
             result = {
               success: true,
               mode: "rename",
    @@ -331,13 +339,13 @@ export async function gitBranchLogic(
             break;
     
           case "show-current":
    -        command = `git -C "${targetPath}" branch --show-current`;
    -        logger.debug(`Executing command: ${command}`, {
    +        args = ["-C", targetPath, "branch", "--show-current"];
    +        logger.debug(`Executing command: git ${args.join(" ")}`, {
               ...context,
               operation,
             });
             try {
    -          const { stdout: currentStdout } = await execAsync(command);
    +          const { stdout: currentStdout } = await execFileAsync("git", args);
               const currentBranchName = currentStdout.trim();
               result = {
                 success: true,
    
  • src/mcp-server/tools/gitCheckout/logic.ts+17 15 modified
    @@ -1,4 +1,4 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import { promisify } from "util";
     import { z } from "zod";
     // Import utils from barrel (logger from ../utils/internal/logger.js)
    @@ -9,7 +9,7 @@ import { RequestContext } from "../../../utils/index.js";
     // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
     import { sanitization } from "../../../utils/index.js";
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the input schema for the git_checkout tool using Zod
     export const GitCheckoutInputSchema = z.object({
    @@ -113,27 +113,26 @@ export async function checkoutGit(
         );
       }
     
    -  // Basic sanitization for branch/path argument
    -  const safeBranchOrPath = input.branchOrPath.replace(/[`$&;*()|<>]/g, ""); // Remove potentially dangerous characters
    -
       try {
         // Construct the git checkout command
    -    let command = `git -C "${targetPath}" checkout`;
    +    const args = ["-C", targetPath, "checkout"];
     
         if (input.force) {
    -      command += " --force";
    +      args.push("--force");
         }
         if (input.newBranch) {
    -      const safeNewBranch = input.newBranch.replace(/[^a-zA-Z0-9_.\-/]/g, ""); // Sanitize new branch name
    -      command += ` -b ${safeNewBranch}`;
    +      args.push("-b", input.newBranch);
         }
     
    -    command += ` ${safeBranchOrPath}`; // Add the target branch/path
    +    args.push(input.branchOrPath); // Add the target branch/path
     
    -    logger.debug(`Executing command: ${command}`, { ...context, operation });
    +    logger.debug(`Executing command: git ${args.join(" ")}`, {
    +      ...context,
    +      operation,
    +    });
     
         // Execute command. Checkout often uses stderr for status messages.
    -    const { stdout, stderr } = await execAsync(command);
    +    const { stdout, stderr } = await execFileAsync("git", args);
     
         const message = stderr.trim() || stdout.trim();
         logger.debug(`Git checkout stdout: ${stdout}`, { ...context, operation });
    @@ -144,9 +143,12 @@ export async function checkoutGit(
         // Get the current branch name after the checkout operation
         let currentBranch: string | undefined;
         try {
    -      const { stdout: branchStdout } = await execAsync(
    -        `git -C "${targetPath}" branch --show-current`,
    -      );
    +      const { stdout: branchStdout } = await execFileAsync("git", [
    +        "-C",
    +        targetPath,
    +        "branch",
    +        "--show-current",
    +      ]);
           currentBranch = branchStdout.trim();
         } catch (e) {
           // This can fail in detached HEAD state, which is not an error for checkout
    
  • src/mcp-server/tools/gitCherryPick/logic.ts+22 11 modified
    @@ -1,4 +1,4 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import { promisify } from "util";
     import { z } from "zod";
     // Import utils from barrel (logger from ../utils/internal/logger.js)
    @@ -9,7 +9,7 @@ import { RequestContext } from "../../../utils/index.js";
     // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
     import { sanitization } from "../../../utils/index.js";
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the input schema for the git_cherry-pick tool using Zod
     export const GitCherryPickInputSchema = z.object({
    @@ -140,20 +140,31 @@ export async function gitCherryPickLogic(
       }
     
       try {
    -    let command = `git -C "${targetPath}" cherry-pick`;
    +    const args = ["-C", targetPath, "cherry-pick"];
     
    -    if (input.mainline) command += ` -m ${input.mainline}`;
    -    if (input.strategy) command += ` -X${input.strategy}`; // Note: -X for strategy options
    -    if (input.noCommit) command += " --no-commit";
    -    if (input.signoff) command += " --signoff";
    +    if (input.mainline) {
    +      args.push("-m", String(input.mainline));
    +    }
    +    if (input.strategy) {
    +      args.push(`-X${input.strategy}`);
    +    } // Note: -X for strategy options
    +    if (input.noCommit) {
    +      args.push("--no-commit");
    +    }
    +    if (input.signoff) {
    +      args.push("--signoff");
    +    }
     
    -    // Add the commit reference(s) - ensure it's treated as a single argument potentially containing special chars like '..'
    -    command += ` "${input.commitRef.replace(/"/g, '\\"')}"`;
    +    // Add the commit reference(s)
    +    args.push(input.commitRef);
     
    -    logger.debug(`Executing command: ${command}`, { ...context, operation });
    +    logger.debug(`Executing command: git ${args.join(" ")}`, {
    +      ...context,
    +      operation,
    +    });
     
         try {
    -      const { stdout, stderr } = await execAsync(command);
    +      const { stdout, stderr } = await execFileAsync("git", args);
           // Check stdout/stderr for conflict messages, although exit code 0 usually means success
           const output = stdout + stderr;
           const conflicts = /conflict/i.test(output);
    
  • src/mcp-server/tools/gitClean/logic.ts+11 8 modified
    @@ -1,4 +1,4 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import { promisify } from "util";
     import { z } from "zod";
     // Import utils from barrel (logger from ../utils/internal/logger.js)
    @@ -9,7 +9,7 @@ import { RequestContext } from "../../../utils/index.js";
     // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
     import { sanitization } from "../../../utils/index.js";
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the input schema for the git_clean tool using Zod
     // No refinements needed here, but the 'force' check is critical in the logic
    @@ -155,20 +155,23 @@ export async function gitCleanLogic(
       try {
         // Construct the command
         // Force (-f) is always added because the logic checks input.force
    -    let command = `git -C "${targetPath}" clean -f`;
    +    const args = ["-C", targetPath, "clean", "-f"];
         if (input.dryRun) {
    -      command += " -n";
    +      args.push("-n");
         }
         if (input.directories) {
    -      command += " -d";
    +      args.push("-d");
         }
         if (input.ignored) {
    -      command += " -x";
    +      args.push("-x");
         }
     
    -    logger.debug(`Executing command: ${command}`, { ...context, operation });
    +    logger.debug(`Executing command: git ${args.join(" ")}`, {
    +      ...context,
    +      operation,
    +    });
     
    -    const { stdout, stderr } = await execAsync(command);
    +    const { stdout, stderr } = await execFileAsync("git", args);
     
         if (stderr) {
           // Log stderr as warning, as git clean might report non-fatal issues here
    
  • src/mcp-server/tools/gitClone/logic.ts+15 11 modified
    @@ -1,4 +1,4 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import fs from "fs/promises";
     import { promisify } from "util";
     import { z } from "zod";
    @@ -10,7 +10,7 @@ import { RequestContext } from "../../../utils/index.js";
     // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
     import { sanitization } from "../../../utils/index.js";
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the input schema for the git_clone tool using Zod
     export const GitCloneInputSchema = z.object({
    @@ -152,24 +152,28 @@ export async function gitCloneLogic(
     
       try {
         // Construct the git clone command
    -    // Use placeholders and pass args safely if possible, but exec requires string command. Be careful with quoting.
    -    let command = `git clone`;
    +    const args = ["clone"];
         if (input.quiet) {
    -      command += " --quiet";
    +      args.push("--quiet");
         }
         if (input.branch) {
    -      command += ` --branch "${input.branch.replace(/"/g, '\\"')}"`;
    +      args.push("--branch", input.branch);
         }
         if (input.depth) {
    -      command += ` --depth ${input.depth}`;
    +      args.push("--depth", String(input.depth));
         }
    -    // Add repo URL and target path (ensure they are quoted)
    -    command += ` "${sanitizedRepoUrl}" "${sanitizedTargetPath}"`;
    +    // Add repo URL and target path
    +    args.push(sanitizedRepoUrl, sanitizedTargetPath);
     
    -    logger.debug(`Executing command: ${command}`, { ...context, operation });
    +    logger.debug(`Executing command: git ${args.join(" ")}`, {
    +      ...context,
    +      operation,
    +    });
     
         // Increase timeout for clone operations as they can take time
    -    const { stdout, stderr } = await execAsync(command, { timeout: 300000 }); // 5 minutes timeout
    +    const { stdout, stderr } = await execFileAsync("git", args, {
    +      timeout: 300000,
    +    }); // 5 minutes timeout
     
         if (stderr && !input.quiet) {
           // Stderr often contains progress info, log as info if quiet is false
    
  • src/mcp-server/tools/gitCommit/logic.ts+36 65 modified
    @@ -1,4 +1,4 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import { promisify } from "util";
     import { z } from "zod";
     import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
    @@ -11,7 +11,7 @@ import { sanitization } from "../../../utils/index.js";
     // Import config to check signing flag
     import { config } from "../../../config/index.js";
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the input schema for the git_commit tool using Zod
     export const GitCommitInputSchema = z.object({
    @@ -157,15 +157,12 @@ export async function commitGitChanges(
                 sanitization.sanitizePath(file, { rootDir: targetPath })
                   .sanitizedPath,
             ); // Sanitize relative to repo root
    -        const filesToAddString = sanitizedFiles
    -          .map((file) => `"${file}"`)
    -          .join(" "); // Quote paths for safety
    -        const addCommand = `git -C "${targetPath}" add -- ${filesToAddString}`;
    -        logger.debug(`Executing git add command: ${addCommand}`, {
    +        const addArgs = ["-C", targetPath, "add", "--", ...sanitizedFiles];
    +        logger.debug(`Executing git add command: git ${addArgs.join(" ")}`, {
               ...context,
               operation,
             });
    -        await execAsync(addCommand);
    +        await execFileAsync("git", addArgs);
             logger.info(
               `Successfully staged specified files: ${sanitizedFiles.join(", ")}`,
               { ...context, operation },
    @@ -187,49 +184,37 @@ export async function commitGitChanges(
         }
         // --- End staging files ---
     
    -    // Escape message for shell safety
    -    const escapeShellArg = (arg: string): string => {
    -      // Escape backslashes first, then other special chars
    -      return arg
    -        .replace(/\\/g, "\\\\")
    -        .replace(/"/g, '\\"')
    -        .replace(/`/g, "\\`")
    -        .replace(/\$/g, "\\$");
    -    };
    -    const escapedMessage = escapeShellArg(input.message);
    -
         // Construct the git commit command using the resolved targetPath
    -    let command = `git -C "${targetPath}" commit -m "${escapedMessage}"`;
    +    const args = ["-C", targetPath];
    +
    +    if (input.author) {
    +      args.push(
    +        "-c",
    +        `user.name=${input.author.name}`,
    +        "-c",
    +        `user.email=${input.author.email}`,
    +      );
    +    }
    +
    +    args.push("commit", "-m", input.message);
     
         if (input.allowEmpty) {
    -      command += " --allow-empty";
    +      args.push("--allow-empty");
         }
         if (input.amend) {
    -      command += " --amend --no-edit";
    -    }
    -    if (input.author) {
    -      // Escape author details as well
    -      const escapedAuthorName = escapeShellArg(input.author.name);
    -      const escapedAuthorEmail = escapeShellArg(input.author.email); // Email typically safe, but escape anyway
    -      // Use -c flags to override author for this commit, using the already escaped message
    -      command = `git -C "${targetPath}" -c user.name="${escapedAuthorName}" -c user.email="${escapedAuthorEmail}" commit -m "${escapedMessage}"`;
    +      args.push("--amend", "--no-edit");
         }
    -    // Append common flags (ensure they are appended to the potentially modified command from author block)
    -    if (input.allowEmpty && !command.includes(" --allow-empty"))
    -      command += " --allow-empty";
    -    if (input.amend && !command.includes(" --amend"))
    -      command += " --amend --no-edit"; // Avoid double adding if author block modified command
     
         // Append signing flag if configured via GIT_SIGN_COMMITS env var
         if (config.gitSignCommits) {
    -      command += " -S"; // Add signing flag (-S)
    +      args.push("-S"); // Add signing flag (-S)
           logger.info(
             "Signing enabled via GIT_SIGN_COMMITS=true, adding -S flag.",
             { ...context, operation },
           );
         }
     
    -    logger.debug(`Executing initial command attempt: ${command}`, {
    +    logger.debug(`Executing initial command attempt: git ${args.join(" ")}`, {
           ...context,
           operation,
         });
    @@ -240,7 +225,7 @@ export async function commitGitChanges(
     
         try {
           // Initial attempt (potentially with -S flag)
    -      const execResult = await execAsync(command);
    +      const execResult = await execFileAsync("git", args);
           stdout = execResult.stdout;
           stderr = execResult.stderr;
         } catch (error: any) {
    @@ -255,38 +240,17 @@ export async function commitGitChanges(
               { ...context, operation, initialError: initialErrorMessage },
             );
     
    -        // Construct command *without* -S flag, using escaped message/author
    -        const escapeShellArg = (arg: string): string => {
    -          return arg
    -            .replace(/\\/g, "\\\\")
    -            .replace(/"/g, '\\"')
    -            .replace(/`/g, "\\`")
    -            .replace(/\$/g, "\\$");
    -        };
    -        const escapedMessage = escapeShellArg(input.message);
    -        let unsignedCommand = `git -C "${targetPath}" commit -m "${escapedMessage}"`;
    -
    -        if (input.allowEmpty) unsignedCommand += " --allow-empty";
    -        if (input.amend) unsignedCommand += " --amend --no-edit";
    -        if (input.author) {
    -          const escapedAuthorName = escapeShellArg(input.author.name);
    -          const escapedAuthorEmail = escapeShellArg(input.author.email);
    -          unsignedCommand = `git -C "${targetPath}" -c user.name="${escapedAuthorName}" -c user.email="${escapedAuthorEmail}" commit -m "${escapedMessage}"`;
    -          // Re-append common flags if author block overwrote command
    -          if (input.allowEmpty && !unsignedCommand.includes(" --allow-empty"))
    -            unsignedCommand += " --allow-empty";
    -          if (input.amend && !unsignedCommand.includes(" --amend"))
    -            unsignedCommand += " --amend --no-edit";
    -        }
    +        // Construct command *without* -S flag
    +        const unsignedArgs = args.filter((arg) => arg !== "-S");
     
             logger.debug(
    -          `Executing unsigned fallback command: ${unsignedCommand}`,
    +          `Executing unsigned fallback command: git ${unsignedArgs.join(" ")}`,
               { ...context, operation },
             );
     
             try {
               // Retry commit without signing
    -          const fallbackResult = await execAsync(unsignedCommand);
    +          const fallbackResult = await execFileAsync("git", unsignedArgs);
               stdout = fallbackResult.stdout;
               stderr = fallbackResult.stderr;
               // Add a note to the status message indicating signing was skipped
    @@ -359,12 +323,19 @@ export async function commitGitChanges(
         if (commitHash) {
           try {
             // Get the list of files included in this specific commit
    -        const showCommand = `git -C "${targetPath}" show --pretty="" --name-only ${commitHash}`;
    -        logger.debug(`Executing git show command: ${showCommand}`, {
    +        const showArgs = [
    +          "-C",
    +          targetPath,
    +          "show",
    +          "--pretty=",
    +          "--name-only",
    +          commitHash,
    +        ];
    +        logger.debug(`Executing git show command: git ${showArgs.join(" ")}`, {
               ...context,
               operation,
             });
    -        const { stdout: showStdout } = await execAsync(showCommand);
    +        const { stdout: showStdout } = await execFileAsync("git", showArgs);
             committedFiles = showStdout.trim().split("\n").filter(Boolean); // Split by newline, remove empty lines
             logger.debug(`Retrieved committed files list for ${commitHash}`, {
               ...context,
    
  • src/mcp-server/tools/gitDiff/logic.ts+41 21 modified
    @@ -1,4 +1,4 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import { promisify } from "util";
     import { z } from "zod";
     // Import utils from barrel (logger from ../utils/internal/logger.js)
    @@ -9,7 +9,7 @@ import { RequestContext } from "../../../utils/index.js";
     // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
     import { sanitization } from "../../../utils/index.js";
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the base input schema without refinement
     const GitDiffInputBaseSchema = z.object({
    @@ -144,35 +144,38 @@ export async function diffGitChanges(
     
       try {
         // Construct the standard git diff command
    -    let standardDiffCommand = `git -C "${targetPath}" diff`;
    +    const standardDiffArgs = ["-C", targetPath, "diff"];
     
         if (input.staged) {
    -      standardDiffCommand += " --staged"; // Or --cached
    +      standardDiffArgs.push("--staged"); // Or --cached
         } else {
           // Add commit references if not doing staged diff
           if (safeCommit1) {
    -        standardDiffCommand += ` ${safeCommit1}`;
    +        standardDiffArgs.push(safeCommit1);
           }
           if (safeCommit2) {
    -        standardDiffCommand += ` ${safeCommit2}`;
    +        standardDiffArgs.push(safeCommit2);
           }
         }
     
         // Add file path limiter if provided for standard diff
         // Note: `input.file` will not apply to the untracked files part unless we explicitly filter them.
         // For simplicity, `includeUntracked` will show all untracked files if `input.file` is also set.
         if (safeFile) {
    -      standardDiffCommand += ` -- "${safeFile}"`; // Use '--' to separate paths from revisions
    +      standardDiffArgs.push("--", safeFile); // Use '--' to separate paths from revisions
         }
     
    -    logger.debug(`Executing standard diff command: ${standardDiffCommand}`, {
    -      ...context,
    -      operation,
    -    });
    -    const { stdout: standardStdout, stderr: standardStderr } = await execAsync(
    -      standardDiffCommand,
    -      { maxBuffer: 1024 * 1024 * 20 },
    +    logger.debug(
    +      `Executing standard diff command: git ${standardDiffArgs.join(" ")}`,
    +      {
    +        ...context,
    +        operation,
    +      },
         );
    +    const { stdout: standardStdout, stderr: standardStderr } =
    +      await execFileAsync("git", standardDiffArgs, {
    +        maxBuffer: 1024 * 1024 * 20,
    +      });
     
         if (standardStderr) {
           logger.warning(`Git diff (standard) stderr: ${standardStderr}`, {
    @@ -185,10 +188,18 @@ export async function diffGitChanges(
         // Handle untracked files if requested
         if (input.includeUntracked) {
           logger.debug("Including untracked files.", { ...context, operation });
    -      const listUntrackedCommand = `git -C "${targetPath}" ls-files --others --exclude-standard`;
    +      const listUntrackedArgs = [
    +        "-C",
    +        targetPath,
    +        "ls-files",
    +        "--others",
    +        "--exclude-standard",
    +      ];
           try {
    -        const { stdout: untrackedFilesStdOut } =
    -          await execAsync(listUntrackedCommand);
    +        const { stdout: untrackedFilesStdOut } = await execFileAsync(
    +          "git",
    +          listUntrackedArgs,
    +        );
             const untrackedFiles = untrackedFilesStdOut
               .trim()
               .split("\n")
    @@ -210,14 +221,23 @@ export async function diffGitChanges(
                 // Skip if file path becomes empty after sanitization (unlikely but safe)
                 if (!safeUntrackedFile) continue;
     
    -            const untrackedDiffCommand = `git -C "${targetPath}" diff --no-index /dev/null "${safeUntrackedFile}"`;
    +            const untrackedDiffArgs = [
    +              "-C",
    +              targetPath,
    +              "diff",
    +              "--no-index",
    +              "/dev/null",
    +              safeUntrackedFile,
    +            ];
                 logger.debug(
    -              `Executing diff for untracked file: ${untrackedDiffCommand}`,
    +              `Executing diff for untracked file: git ${untrackedDiffArgs.join(" ")}`,
                   { ...context, operation, file: safeUntrackedFile },
                 );
                 try {
    -              const { stdout: untrackedFileDiffOut } =
    -                await execAsync(untrackedDiffCommand);
    +              const { stdout: untrackedFileDiffOut } = await execFileAsync(
    +                "git",
    +                untrackedDiffArgs,
    +              );
                   individualUntrackedDiffs += untrackedFileDiffOut;
                   untrackedFilesCount++;
                 } catch (untrackedError: any) {
    
  • src/mcp-server/tools/gitFetch/logic.ts+13 13 modified
    @@ -1,4 +1,4 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import { promisify } from "util";
     import { z } from "zod";
     // Import utils from barrel (logger from ../utils/internal/logger.js)
    @@ -9,7 +9,7 @@ import { RequestContext } from "../../../utils/index.js";
     // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
     import { sanitization } from "../../../utils/index.js";
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the input schema for the git_fetch tool using Zod
     export const GitFetchInputSchema = z.object({
    @@ -111,30 +111,30 @@ export async function fetchGitRemote(
         );
       }
     
    -  // Basic sanitization for remote name
    -  const safeRemote = input.remote?.replace(/[^a-zA-Z0-9_.\-/]/g, "");
    -
       try {
         // Construct the git fetch command
    -    let command = `git -C "${targetPath}" fetch`;
    +    const args = ["-C", targetPath, "fetch"];
     
         if (input.prune) {
    -      command += " --prune";
    +      args.push("--prune");
         }
         if (input.tags) {
    -      command += " --tags";
    +      args.push("--tags");
         }
         if (input.all) {
    -      command += " --all";
    -    } else if (safeRemote) {
    -      command += ` ${safeRemote}`; // Fetch specific remote if 'all' is not used
    +      args.push("--all");
    +    } else if (input.remote) {
    +      args.push(input.remote); // Fetch specific remote if 'all' is not used
         }
         // If neither 'all' nor 'remote' is specified, git fetch defaults to 'origin' or configured upstream.
     
    -    logger.debug(`Executing command: ${command}`, { ...context, operation });
    +    logger.debug(`Executing command: git ${args.join(" ")}`, {
    +      ...context,
    +      operation,
    +    });
     
         // Execute command. Fetch output is primarily on stderr.
    -    const { stdout, stderr } = await execAsync(command);
    +    const { stdout, stderr } = await execFileAsync("git", args);
     
         logger.debug(`Git fetch stdout: ${stdout}`, { ...context, operation }); // stdout is usually empty
         if (stderr) {
    
  • src/mcp-server/tools/gitInit/logic.ts+12 9 modified
    @@ -1,4 +1,4 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import fs from "fs/promises";
     import path from "path";
     import { promisify } from "util";
    @@ -11,7 +11,7 @@ import { RequestContext } from "../../../utils/index.js";
     // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
     import { sanitization } from "../../../utils/index.js";
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the input schema for the git_init tool using Zod
     export const GitInitInputSchema = z.object({
    @@ -119,23 +119,26 @@ export async function gitInitLogic(
     
       try {
         // Construct the git init command
    -    let command = `git init`;
    +    const args: string[] = ["init"];
         if (input.quiet) {
    -      command += " --quiet";
    +      args.push("--quiet");
         }
         if (input.bare) {
    -      command += " --bare";
    +      args.push("--bare");
         }
         // Determine the initial branch name, defaulting to 'main' if not provided
         const branchNameToUse = input.initialBranch || "main";
    -    command += ` -b "${branchNameToUse.replace(/"/g, '\\"')}"`;
    +    args.push("-b", branchNameToUse);
     
         // Add the target directory path at the end
    -    command += ` "${targetPath}"`;
    +    args.push(targetPath);
     
    -    logger.debug(`Executing command: ${command}`, { ...context, operation });
    +    logger.debug(`Executing command: git ${args.join(" ")}`, {
    +      ...context,
    +      operation,
    +    });
     
    -    const { stdout, stderr } = await execAsync(command);
    +    const { stdout, stderr } = await execFileAsync("git", args);
     
         if (stderr && !input.quiet) {
           // Log stderr as warning but proceed, as init might still succeed (e.g., reinitializing)
    
  • src/mcp-server/tools/gitLog/logic.ts+18 28 modified
    @@ -1,4 +1,4 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import { promisify } from "util";
     import { z } from "zod";
     // Import utils from barrel (logger from ../utils/internal/logger.js)
    @@ -9,7 +9,7 @@ import { RequestContext } from "../../../utils/index.js";
     // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
     import { sanitization } from "../../../utils/index.js";
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the structure for a single commit entry
     export const CommitEntrySchema = z.object({
    @@ -171,53 +171,43 @@ export async function logGitHistory(
       }
     
       try {
    -    let command: string;
    +    const args = ["-C", targetPath, "log"];
         let isRawOutput = false; // Flag to indicate if we should parse or return raw
     
         if (input.showSignature) {
           isRawOutput = true;
    -      command = `git -C "${targetPath}" log --show-signature`;
    +      args.push("--show-signature");
           logger.info("Show signature requested, returning raw output.", {
             ...context,
             operation,
           });
    -      // Append other filters if provided
    -      if (input.maxCount) command += ` -n ${input.maxCount}`;
    -      if (input.author)
    -        command += ` --author="${input.author.replace(/[`"$&;*()|<>]/g, "")}"`;
    -      if (input.since)
    -        command += ` --since="${input.since.replace(/[`"$&;*()|<>]/g, "")}"`;
    -      if (input.until)
    -        command += ` --until="${input.until.replace(/[`"$&;*()|<>]/g, "")}"`;
    -      if (input.branchOrFile)
    -        command += ` ${input.branchOrFile.replace(/[`"$&;*()|<>]/g, "")}`;
         } else {
    -      // Construct the git log command with the fixed format for parsing
    -      command = `git -C "${targetPath}" log ${GIT_LOG_FORMAT}`;
    -      if (input.maxCount) command += ` -n ${input.maxCount}`;
    +      args.push(GIT_LOG_FORMAT);
    +    }
    +
    +    if (input.maxCount) {
    +      args.push(`-n${input.maxCount}`);
         }
         if (input.author) {
    -      // Basic sanitization for author string
    -      const safeAuthor = input.author.replace(/[`"$&;*()|<>]/g, "");
    -      command += ` --author="${safeAuthor}"`;
    +      args.push(`--author=${input.author}`);
         }
         if (input.since) {
    -      const safeSince = input.since.replace(/[`"$&;*()|<>]/g, "");
    -      command += ` --since="${safeSince}"`;
    +      args.push(`--since=${input.since}`);
         }
         if (input.until) {
    -      const safeUntil = input.until.replace(/[`"$&;*()|<>]/g, "");
    -      command += ` --until="${safeUntil}"`;
    +      args.push(`--until=${input.until}`);
         }
         if (input.branchOrFile) {
    -      const safeBranchOrFile = input.branchOrFile.replace(/[`"$&;*()|<>]/g, "");
    -      command += ` ${safeBranchOrFile}`; // Add branch or file path at the end
    +      args.push(input.branchOrFile);
         }
     
    -    logger.debug(`Executing command: ${command}`, { ...context, operation });
    +    logger.debug(`Executing command: git ${args.join(" ")}`, {
    +      ...context,
    +      operation,
    +    });
     
         // Increase maxBuffer if logs can be large
    -    const { stdout, stderr } = await execAsync(command, {
    +    const { stdout, stderr } = await execFileAsync("git", args, {
           maxBuffer: 1024 * 1024 * 10,
         }); // 10MB buffer
     
    
  • src/mcp-server/tools/gitMerge/logic.ts+17 10 modified
    @@ -1,4 +1,4 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import { promisify } from "util";
     import { z } from "zod";
     // Import utils from barrel (logger from ../utils/internal/logger.js)
    @@ -10,7 +10,7 @@ import { RequestContext } from "../../../utils/index.js";
     import path from "path"; // Import path module
     import { sanitization } from "../../../utils/index.js";
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the input schema for the git_merge tool
     export const GitMergeInputSchema = z.object({
    @@ -150,31 +150,38 @@ export async function gitMergeLogic(
       }
     
       // --- Construct the git merge command ---
    -  let command = `git -C "${targetPath}" merge`;
    +  const args = ["-C", targetPath, "merge"];
     
       if (input.abort) {
    -    command += " --abort";
    +    args.push("--abort");
       } else {
         // Standard merge options
    -    if (input.noFf) command += " --no-ff";
    -    if (input.squash) command += " --squash";
    +    if (input.noFf) {
    +      args.push("--no-ff");
    +    }
    +    if (input.squash) {
    +      args.push("--squash");
    +    }
         if (input.commitMessage && !input.squash) {
           // Commit message only relevant if not squashing (squash requires separate commit)
    -      command += ` -m "${input.commitMessage.replace(/"/g, '\\"')}"`;
    +      args.push("-m", input.commitMessage);
         } else if (input.squash && input.commitMessage) {
           logger.warning(
             "Commit message provided with --squash, but it will be ignored. Squash requires a separate commit.",
             { ...context, operation },
           );
         }
    -    command += ` "${input.branch.replace(/"/g, '\\"')}"`; // Add branch to merge
    +    args.push(input.branch); // Add branch to merge
       }
     
    -  logger.debug(`Executing command: ${command}`, { ...context, operation });
    +  logger.debug(`Executing command: git ${args.join(" ")}`, {
    +    ...context,
    +    operation,
    +  });
     
       // --- Execute and Parse ---
       try {
    -    const { stdout, stderr } = await execAsync(command);
    +    const { stdout, stderr } = await execFileAsync("git", args);
         logger.debug(`Command stdout: ${stdout}`, { ...context, operation });
         if (stderr)
           logger.debug(`Command stderr: ${stderr}`, { ...context, operation }); // Log stderr even on success
    
  • src/mcp-server/tools/gitPull/logic.ts+13 14 modified
    @@ -1,4 +1,4 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import { promisify } from "util";
     import { z } from "zod";
     // Import utils from barrel (logger from ../utils/internal/logger.js)
    @@ -9,7 +9,7 @@ import { RequestContext } from "../../../utils/index.js";
     // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
     import { sanitization } from "../../../utils/index.js";
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the input schema for the git_pull tool using Zod
     export const GitPullInputSchema = z.object({
    @@ -125,35 +125,34 @@ export async function pullGitChanges(
     
       try {
         // Construct the git pull command
    -    let command = `git -C "${targetPath}" pull`;
    +    const args = ["-C", targetPath, "pull"];
     
         if (input.rebase) {
    -      command += " --rebase";
    +      args.push("--rebase");
         }
         if (input.ffOnly) {
    -      command += " --ff-only";
    +      args.push("--ff-only");
         }
         if (input.remote) {
    -      // Sanitize remote and branch names - basic alphanumeric + common chars
    -      const safeRemote = input.remote.replace(/[^a-zA-Z0-9_.\-/]/g, "");
    -      command += ` ${safeRemote}`;
    +      args.push(input.remote);
           if (input.branch) {
    -        const safeBranch = input.branch.replace(/[^a-zA-Z0-9_.\-/]/g, "");
    -        command += ` ${safeBranch}`;
    +        args.push(input.branch);
           }
         } else if (input.branch) {
           // If only branch is specified, assume 'origin' or tracked remote
    -      const safeBranch = input.branch.replace(/[^a-zA-Z0-9_.\-/]/g, "");
    -      command += ` origin ${safeBranch}`; // Defaulting to origin if remote not specified but branch is
    +      args.push("origin", input.branch); // Defaulting to origin if remote not specified but branch is
           logger.warning(
             `Remote not specified, defaulting to 'origin' for branch pull`,
             { ...context, operation },
           );
         }
     
    -    logger.debug(`Executing command: ${command}`, { ...context, operation });
    +    logger.debug(`Executing command: git ${args.join(" ")}`, {
    +      ...context,
    +      operation,
    +    });
     
    -    const { stdout, stderr } = await execAsync(command);
    +    const { stdout, stderr } = await execFileAsync("git", args);
     
         logger.debug(`Git pull stdout: ${stdout}`, { ...context, operation });
         if (stderr) {
    
  • src/mcp-server/tools/gitPush/logic.ts+18 24 modified
    @@ -1,4 +1,4 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import { promisify } from "util";
     import { z } from "zod";
     // Import utils from barrel (logger from ../utils/internal/logger.js)
    @@ -9,7 +9,7 @@ import { RequestContext } from "../../../utils/index.js";
     // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
     import { sanitization } from "../../../utils/index.js";
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the input schema for the git_push tool using Zod
     export const GitPushInputSchema = z.object({
    @@ -171,55 +171,49 @@ export async function pushGitChanges(
     
       try {
         // Construct the git push command
    -    let command = `git -C "${targetPath}" push`;
    +    const args = ["-C", targetPath, "push"];
     
         if (input.force) {
    -      command += " --force";
    +      args.push("--force");
         } else if (input.forceWithLease) {
    -      command += " --force-with-lease";
    +      args.push("--force-with-lease");
         }
     
         if (input.setUpstream) {
    -      command += " --set-upstream";
    +      args.push("--set-upstream");
         }
         if (input.tags) {
    -      command += " --tags";
    +      args.push("--tags");
         }
         if (input.delete) {
    -      command += " --delete";
    +      args.push("--delete");
         }
     
         // Add remote and branch specification
    -    const remote = input.remote
    -      ? input.remote.replace(/[^a-zA-Z0-9_.\-/]/g, "")
    -      : "origin"; // Default to origin
    -    command += ` ${remote}`;
    +    const remote = input.remote || "origin"; // Default to origin
    +    args.push(remote);
     
         if (input.branch) {
    -      const localBranch = input.branch.replace(/[^a-zA-Z0-9_.\-/]/g, "");
    -      command += ` ${localBranch}`;
           if (input.remoteBranch && !input.delete) {
    -        // remoteBranch only makes sense if not deleting
    -        const remoteBranch = input.remoteBranch.replace(
    -          /[^a-zA-Z0-9_.\-/]/g,
    -          "",
    -        );
    -        command += `:${remoteBranch}`;
    +        args.push(`${input.branch}:${input.remoteBranch}`);
    +      } else {
    +        args.push(input.branch);
           }
         } else if (!input.tags && !input.delete) {
           // If no branch, tags, or delete specified, push the current branch by default
    -      // Git might handle this automatically, but being explicit can be clearer
    -      // command += ' HEAD'; // Or let git figure out the default push behavior
           logger.debug(
             "No specific branch, tags, or delete specified. Relying on default git push behavior for current branch.",
             { ...context, operation },
           );
         }
     
    -    logger.debug(`Executing command: ${command}`, { ...context, operation });
    +    logger.debug(`Executing command: git ${args.join(" ")}`, {
    +      ...context,
    +      operation,
    +    });
     
         // Execute command. Note: Git push often uses stderr for progress and success messages.
    -    const { stdout, stderr } = await execAsync(command);
    +    const { stdout, stderr } = await execFileAsync("git", args);
     
         logger.debug(`Git push stdout: ${stdout}`, { ...context, operation });
         if (stderr) {
    
  • src/mcp-server/tools/gitRebase/logic.ts+29 16 modified
    @@ -1,4 +1,4 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import { promisify } from "util";
     import { z } from "zod";
     // Import utils from barrel (logger from ../utils/internal/logger.js)
    @@ -9,7 +9,7 @@ import { RequestContext } from "../../../utils/index.js";
     // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
     import { sanitization } from "../../../utils/index.js";
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the BASE input schema for the git_rebase tool using Zod
     export const GitRebaseBaseSchema = z.object({
    @@ -182,28 +182,38 @@ export async function gitRebaseLogic(
       }
     
       try {
    -    let command = `git -C "${targetPath}" rebase`;
    +    const args = ["-C", targetPath, "rebase"];
     
         switch (input.mode) {
           case "start":
    -        if (input.interactive) command += " -i";
    -        if (input.strategy) command += ` --strategy=${input.strategy}`;
    -        if (input.strategyOption) command += ` -X${input.strategyOption}`; // Note: -X for strategy options
    -        if (input.onto)
    -          command += ` --onto "${input.onto.replace(/"/g, '\\"')}"`;
    +        if (input.interactive) {
    +          args.push("-i");
    +        }
    +        if (input.strategy) {
    +          args.push(`--strategy=${input.strategy}`);
    +        }
    +        if (input.strategyOption) {
    +          args.push(`-X${input.strategyOption}`);
    +        } // Note: -X for strategy options
    +        if (input.onto) {
    +          args.push("--onto", input.onto);
    +        }
             // Upstream is required by refine unless interactive
    -        if (input.upstream)
    -          command += ` "${input.upstream.replace(/"/g, '\\"')}"`;
    -        if (input.branch) command += ` "${input.branch.replace(/"/g, '\\"')}"`;
    +        if (input.upstream) {
    +          args.push(input.upstream);
    +        }
    +        if (input.branch) {
    +          args.push(input.branch);
    +        }
             break;
           case "continue":
    -        command += " --continue";
    +        args.push("--continue");
             break;
           case "abort":
    -        command += " --abort";
    +        args.push("--abort");
             break;
           case "skip":
    -        command += " --skip";
    +        args.push("--skip");
             break;
           default:
             // Should not happen due to Zod validation
    @@ -214,10 +224,13 @@ export async function gitRebaseLogic(
             );
         }
     
    -    logger.debug(`Executing command: ${command}`, { ...context, operation });
    +    logger.debug(`Executing command: git ${args.join(" ")}`, {
    +      ...context,
    +      operation,
    +    });
     
         try {
    -      const { stdout, stderr } = await execAsync(command);
    +      const { stdout, stderr } = await execFileAsync("git", args);
           const output = stdout + stderr;
     
           const message = `Rebase ${input.mode} executed successfully. Output: ${output.trim()}`;
    
  • src/mcp-server/tools/gitRemote/logic.ts+15 15 modified
    @@ -1,10 +1,10 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import { promisify } from "util";
     import { z } from "zod";
     import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Direct import for types-global
     import { logger, RequestContext, sanitization } from "../../../utils/index.js"; // Logger (./utils/internal/logger.js) & RequestContext (./utils/internal/requestContext.js) & sanitization (./utils/security/sanitization.js)
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the input schema for the git_remote tool using Zod
     export const GitRemoteInputSchema = z.object({
    @@ -139,17 +139,17 @@ export async function gitRemoteLogic(
       }
     
       try {
    -    let command: string;
    +    let args: string[];
         let result: GitRemoteResult;
     
         switch (input.mode) {
           case "list":
    -        command = `git -C "${targetPath}" remote -v`;
    -        logger.debug(`Executing command: ${command}`, {
    +        args = ["-C", targetPath, "remote", "-v"];
    +        logger.debug(`Executing command: git ${args.join(" ")}`, {
               ...context,
               operation,
             });
    -        const { stdout: listStdout } = await execAsync(command);
    +        const { stdout: listStdout } = await execFileAsync("git", args);
             const remotes: GitRemoteListResult["remotes"] = [];
             const lines = listStdout.trim().split("\n");
             const remoteMap = new Map<
    @@ -202,12 +202,12 @@ export async function gitRemoteLogic(
                 { context, operation },
               );
             }
    -        command = `git -C "${targetPath}" remote add "${input.name}" "${input.url}"`;
    -        logger.debug(`Executing command: ${command}`, {
    +        args = ["-C", targetPath, "remote", "add", input.name, input.url];
    +        logger.debug(`Executing command: git ${args.join(" ")}`, {
               ...context,
               operation,
             });
    -        await execAsync(command);
    +        await execFileAsync("git", args);
             result = {
               success: true,
               mode: "add",
    @@ -230,12 +230,12 @@ export async function gitRemoteLogic(
                 { context, operation },
               );
             }
    -        command = `git -C "${targetPath}" remote remove "${input.name}"`;
    -        logger.debug(`Executing command: ${command}`, {
    +        args = ["-C", targetPath, "remote", "remove", input.name];
    +        logger.debug(`Executing command: git ${args.join(" ")}`, {
               ...context,
               operation,
             });
    -        await execAsync(command);
    +        await execFileAsync("git", args);
             result = {
               success: true,
               mode: "remove",
    @@ -258,12 +258,12 @@ export async function gitRemoteLogic(
                 { context, operation },
               );
             }
    -        command = `git -C "${targetPath}" remote show "${input.name}"`;
    -        logger.debug(`Executing command: ${command}`, {
    +        args = ["-C", targetPath, "remote", "show", input.name];
    +        logger.debug(`Executing command: git ${args.join(" ")}`, {
               ...context,
               operation,
             });
    -        const { stdout: showStdout } = await execAsync(command);
    +        const { stdout: showStdout } = await execFileAsync("git", args);
             result = { success: true, mode: "show", details: showStdout.trim() };
             break;
     
    
  • src/mcp-server/tools/gitReset/logic.ts+11 11 modified
    @@ -1,10 +1,10 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import { promisify } from "util";
     import { z } from "zod";
     import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Direct import for types-global
     import { logger, RequestContext, sanitization } from "../../../utils/index.js"; // logger (./utils/internal/logger.js), RequestContext (./utils/internal/requestContext.js), sanitization (./utils/security/sanitization.js)
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the reset modes
     const ResetModeEnum = z.enum(["soft", "mixed", "hard", "merge", "keep"]);
    @@ -109,29 +109,29 @@ export async function resetGitState(
         );
       }
     
    -  // Basic sanitization for commit ref
    -  const safeCommit = input.commit?.replace(/[`$&;*()|<>]/g, "");
    -
       try {
         // Construct the git reset command
    -    let command = `git -C "${targetPath}" reset`;
    +    const args = ["-C", targetPath, "reset"];
     
         if (input.mode) {
    -      command += ` --${input.mode}`;
    +      args.push(`--${input.mode}`);
         }
     
    -    if (safeCommit) {
    -      command += ` ${safeCommit}`;
    +    if (input.commit) {
    +      args.push(input.commit);
         }
         // Handling file paths requires careful command construction, often without a commit ref.
         // Example: `git reset HEAD -- path/to/file` or `git reset -- path/to/file` (unstages)
         // For simplicity, this initial version focuses on resetting the whole HEAD/index/tree.
         // Add file path logic here if needed, adjusting command structure.
     
    -    logger.debug(`Executing command: ${command}`, { ...context, operation });
    +    logger.debug(`Executing command: git ${args.join(" ")}`, {
    +      ...context,
    +      operation,
    +    });
     
         // Execute command. Reset output is often minimal on success, but stderr might indicate issues.
    -    const { stdout, stderr } = await execAsync(command);
    +    const { stdout, stderr } = await execFileAsync("git", args);
     
         logger.debug(`Git reset stdout: ${stdout}`, { ...context, operation });
         if (stderr) {
    
  • src/mcp-server/tools/gitSetWorkingDir/logic.ts+12 6 modified
    @@ -1,11 +1,11 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import fs from "fs/promises";
     import { promisify } from "util";
     import { z } from "zod";
     import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Direct import for types-global
     import { RequestContext, logger, sanitization } from "../../../utils/index.js"; // RequestContext (./utils/internal/requestContext.js), logger (./utils/internal/logger.js), sanitization (./utils/security/sanitization.js)
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the Zod schema for input validation
     export const GitSetWorkingDirInputSchema = z.object({
    @@ -111,9 +111,13 @@ export async function gitSetWorkingDirLogic(
       let initializedRepo = false;
     
       try {
    -    const { stdout } = await execAsync("git rev-parse --is-inside-work-tree", {
    -      cwd: sanitizedPath,
    -    });
    +    const { stdout } = await execFileAsync(
    +      "git",
    +      ["rev-parse", "--is-inside-work-tree"],
    +      {
    +        cwd: sanitizedPath,
    +      },
    +    );
         if (stdout.trim() === "true") {
           isGitRepo = true;
           logger.debug("Path is already a Git repository", {
    @@ -141,7 +145,9 @@ export async function gitSetWorkingDirLogic(
           { ...context, operation, path: sanitizedPath },
         );
         try {
    -      await execAsync("git init --initial-branch=main", { cwd: sanitizedPath });
    +      await execFileAsync("git", ["init", "--initial-branch=main"], {
    +        cwd: sanitizedPath,
    +      });
           initializedRepo = true;
           isGitRepo = true; // Now it is a git repo
           logger.info(
    
  • src/mcp-server/tools/gitShow/logic.ts+9 8 modified
    @@ -1,10 +1,10 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import { promisify } from "util";
     import { z } from "zod";
     import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Direct import for types-global
     import { logger, RequestContext, sanitization } from "../../../utils/index.js"; // logger (./utils/internal/logger.js), RequestContext (./utils/internal/requestContext.js), sanitization (./utils/security/sanitization.js)
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the input schema for the git_show tool using Zod
     // No refinements needed here, so we don't need a separate BaseSchema
    @@ -136,17 +136,18 @@ export async function gitShowLogic(
     
       try {
         // Construct the refspec, combining ref and filePath if needed
    -    const refSpec = input.filePath
    -      ? `${input.ref}:"${input.filePath}"`
    -      : `"${input.ref}"`;
    +    const refSpec = input.filePath ? `${input.ref}:${input.filePath}` : input.ref;
     
         // Construct the command
    -    const command = `git -C "${targetPath}" show ${refSpec}`;
    -    logger.debug(`Executing command: ${command}`, { ...context, operation });
    +    const args = ["-C", targetPath, "show", refSpec];
    +    logger.debug(`Executing command: git ${args.join(" ")}`, {
    +      ...context,
    +      operation,
    +    });
     
         // Execute command. Note: git show might write to stderr for non-error info (like commit details before diff)
         // We primarily care about stdout for the content. Errors usually have non-zero exit code.
    -    const { stdout, stderr } = await execAsync(command);
    +    const { stdout, stderr } = await execFileAsync("git", args);
     
         if (stderr) {
           // Log stderr as debug info, as it might contain commit details etc.
    
  • src/mcp-server/tools/gitStash/logic.ts+16 17 modified
    @@ -1,10 +1,10 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import { promisify } from "util";
     import { z } from "zod";
     import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Direct import for types-global
     import { logger, RequestContext, sanitization } from "../../../utils/index.js"; // logger (./utils/internal/logger.js), RequestContext (./utils/internal/requestContext.js), sanitization (./utils/security/sanitization.js)
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the BASE input schema for the git_stash tool using Zod
     export const GitStashBaseSchema = z.object({
    @@ -168,17 +168,17 @@ export async function gitStashLogic(
       }
     
       try {
    -    let command: string;
    +    let args: string[];
         let result: GitStashResult;
     
         switch (input.mode) {
           case "list":
    -        command = `git -C "${targetPath}" stash list`;
    -        logger.debug(`Executing command: ${command}`, {
    +        args = ["-C", targetPath, "stash", "list"];
    +        logger.debug(`Executing command: git ${args.join(" ")}`, {
               ...context,
               operation,
             });
    -        const { stdout: listStdout } = await execAsync(command);
    +        const { stdout: listStdout } = await execFileAsync("git", args);
             const stashes: GitStashListResult["stashes"] = listStdout
               .trim()
               .split("\n")
    @@ -203,13 +203,13 @@ export async function gitStashLogic(
           case "pop":
             // stashRef is validated by Zod refine
             const stashRefApplyPop = input.stashRef!;
    -        command = `git -C "${targetPath}" stash ${input.mode} ${stashRefApplyPop}`;
    -        logger.debug(`Executing command: ${command}`, {
    +        args = ["-C", targetPath, "stash", input.mode, stashRefApplyPop];
    +        logger.debug(`Executing command: git ${args.join(" ")}`, {
               ...context,
               operation,
             });
             try {
    -          const { stdout, stderr } = await execAsync(command);
    +          const { stdout, stderr } = await execFileAsync("git", args);
               // Check stdout/stderr for conflict messages, although exit code 0 usually means success
               const conflicts =
                 /conflict/i.test(stdout) || /conflict/i.test(stderr);
    @@ -250,12 +250,12 @@ export async function gitStashLogic(
           case "drop":
             // stashRef is validated by Zod refine
             const stashRefDrop = input.stashRef!;
    -        command = `git -C "${targetPath}" stash drop ${stashRefDrop}`;
    -        logger.debug(`Executing command: ${command}`, {
    +        args = ["-C", targetPath, "stash", "drop", stashRefDrop];
    +        logger.debug(`Executing command: git ${args.join(" ")}`, {
               ...context,
               operation,
             });
    -        await execAsync(command);
    +        await execFileAsync("git", args);
             result = {
               success: true,
               mode: "drop",
    @@ -265,16 +265,15 @@ export async function gitStashLogic(
             break;
     
           case "save":
    -        command = `git -C "${targetPath}" stash save`;
    +        args = ["-C", targetPath, "stash", "save"];
             if (input.message) {
    -          // Ensure message is properly quoted for the shell
    -          command += ` "${input.message.replace(/"/g, '\\"')}"`;
    +          args.push(input.message);
             }
    -        logger.debug(`Executing command: ${command}`, {
    +        logger.debug(`Executing command: git ${args.join(" ")}`, {
               ...context,
               operation,
             });
    -        const { stdout: saveStdout } = await execAsync(command);
    +        const { stdout: saveStdout } = await execFileAsync("git", args);
             const stashCreated = !/no local changes to save/i.test(saveStdout);
             const saveMessage = stashCreated
               ? `Changes stashed successfully.` +
    
  • src/mcp-server/tools/gitStatus/logic.ts+10 8 modified
    @@ -1,10 +1,10 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import { promisify } from "util";
     import { z } from "zod";
     import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Direct import for types-global
     import { logger, RequestContext, sanitization } from "../../../utils/index.js"; // logger (./utils/internal/logger.js), RequestContext (./utils/internal/requestContext.js), sanitization (./utils/security/sanitization.js)
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the input schema for the git_status tool using Zod
     export const GitStatusInputSchema = z.object({
    @@ -256,11 +256,13 @@ export async function getGitStatus(
     
       try {
         // Using --porcelain=v1 for stable, scriptable output and -b for branch info
    -    // Ensure the path passed to -C is correctly quoted for the shell
    -    const command = `git -C "${targetPath}" status --porcelain=v1 -b`;
    -    logger.debug(`Executing command: ${command}`, { ...context, operation });
    +    const args = ["-C", targetPath, "status", "--porcelain=v1", "-b"];
    +    logger.debug(`Executing command: git ${args.join(" ")}`, {
    +      ...context,
    +      operation,
    +    });
     
    -    const { stdout, stderr } = await execAsync(command);
    +    const { stdout, stderr } = await execFileAsync("git", args);
     
         if (stderr) {
           // Log stderr as warning but proceed to parse stdout
    @@ -283,8 +285,8 @@ export async function getGitStatus(
         // This handles the case of an empty repo after init but before first commit
         if (structuredResult.is_clean && !structuredResult.current_branch) {
           try {
    -        const branchCommand = `git -C "${targetPath}" rev-parse --abbrev-ref HEAD`;
    -        const { stdout: branchStdout } = await execAsync(branchCommand);
    +        const branchArgs = ["-C", targetPath, "rev-parse", "--abbrev-ref", "HEAD"];
    +        const { stdout: branchStdout } = await execFileAsync("git", branchArgs);
             const currentBranchName = branchStdout.trim(); // Renamed variable for clarity
             if (currentBranchName && currentBranchName !== "HEAD") {
               structuredResult.current_branch = currentBranchName;
    
  • src/mcp-server/tools/gitTag/logic.ts+15 15 modified
    @@ -1,10 +1,10 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import { promisify } from "util";
     import { z } from "zod";
     import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Direct import for types-global
     import { logger, RequestContext, sanitization } from "../../../utils/index.js"; // logger (./utils/internal/logger.js), RequestContext (./utils/internal/requestContext.js), sanitization (./utils/security/sanitization.js)
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the base input schema for the git_tag tool using Zod
     // We export this separately to access its .shape for registration
    @@ -190,17 +190,17 @@ export async function gitTagLogic(
       }
     
       try {
    -    let command: string;
    +    let args: string[];
         let result: GitTagResult;
     
         switch (input.mode) {
           case "list":
    -        command = `git -C "${targetPath}" tag --list`;
    -        logger.debug(`Executing command: ${command}`, {
    +        args = ["-C", targetPath, "tag", "--list"];
    +        logger.debug(`Executing command: git ${args.join(" ")}`, {
               ...context,
               operation,
             });
    -        const { stdout: listStdout } = await execAsync(command);
    +        const { stdout: listStdout } = await execFileAsync("git", args);
             const tags = listStdout
               .trim()
               .split("\n")
    @@ -211,20 +211,20 @@ export async function gitTagLogic(
           case "create":
             // TagName is validated by Zod refine
             const tagNameCreate = input.tagName!;
    -        command = `git -C "${targetPath}" tag`;
    +        args = ["-C", targetPath, "tag"];
             if (input.annotate) {
               // Message is validated by Zod refine
    -          command += ` -a -m "${input.message!.replace(/"/g, '\\"')}"`;
    +          args.push("-a", "-m", input.message!);
             }
    -        command += ` "${tagNameCreate}"`;
    +        args.push(tagNameCreate);
             if (input.commitRef) {
    -          command += ` "${input.commitRef}"`;
    +          args.push(input.commitRef);
             }
    -        logger.debug(`Executing command: ${command}`, {
    +        logger.debug(`Executing command: git ${args.join(" ")}`, {
               ...context,
               operation,
             });
    -        await execAsync(command);
    +        await execFileAsync("git", args);
             result = {
               success: true,
               mode: "create",
    @@ -236,12 +236,12 @@ export async function gitTagLogic(
           case "delete":
             // TagName is validated by Zod refine
             const tagNameDelete = input.tagName!;
    -        command = `git -C "${targetPath}" tag -d "${tagNameDelete}"`;
    -        logger.debug(`Executing command: ${command}`, {
    +        args = ["-C", targetPath, "tag", "-d", tagNameDelete];
    +        logger.debug(`Executing command: git ${args.join(" ")}`, {
               ...context,
               operation,
             });
    -        await execAsync(command);
    +        await execFileAsync("git", args);
             result = {
               success: true,
               mode: "delete",
    
  • src/mcp-server/tools/gitWorktree/logic.ts+56 31 modified
    @@ -1,11 +1,11 @@
    -import { exec } from "child_process";
    +import { execFile } from "child_process";
     import { promisify } from "util";
     import { z } from "zod";
     import { logger, sanitization } from "../../../utils/index.js";
     import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
     import { RequestContext } from "../../../utils/index.js";
     
    -const execAsync = promisify(exec);
    +const execFileAsync = promisify(execFile);
     
     // Define the BASE input schema for the git_worktree tool using Zod
     export const GitWorktreeBaseSchema = z.object({
    @@ -279,18 +279,20 @@ export async function gitWorktreeLogic(
       }
     
       try {
    -    let command = `git -C "${targetPath}" worktree `;
    +    let args: string[];
         let result: GitWorktreeResult;
     
         switch (input.mode) {
           case "list":
    -        command += "list";
    -        if (input.verbose) command += " --porcelain"; // Use porcelain for structured output
    -        logger.debug(`Executing command: ${command}`, {
    +        args = ["-C", targetPath, "worktree", "list"];
    +        if (input.verbose) {
    +          args.push("--porcelain");
    +        } // Use porcelain for structured output
    +        logger.debug(`Executing command: git ${args.join(" ")}`, {
               ...context,
               operation,
             });
    -        const { stdout: listStdout } = await execAsync(command);
    +        const { stdout: listStdout } = await execFileAsync("git", args);
             if (input.verbose) {
               const worktrees = parsePorcelainWorktreeList(listStdout);
               result = { success: true, mode: "list", worktrees };
    @@ -320,18 +322,26 @@ export async function gitWorktreeLogic(
               input.worktreePath!,
               { allowAbsolute: true, rootDir: targetPath },
             ).sanitizedPath;
    -        command += `add `;
    -        if (input.force) command += "--force ";
    -        if (input.detach) command += "--detach ";
    -        if (input.newBranch) command += `-b "${input.newBranch}" `;
    -        command += `"${sanitizedWorktreePathAdd}"`;
    -        if (input.commitish) command += ` "${input.commitish}"`;
    -
    -        logger.debug(`Executing command: ${command}`, {
    +        args = ["-C", targetPath, "worktree", "add"];
    +        if (input.force) {
    +          args.push("--force");
    +        }
    +        if (input.detach) {
    +          args.push("--detach");
    +        }
    +        if (input.newBranch) {
    +          args.push("-b", input.newBranch);
    +        }
    +        args.push(sanitizedWorktreePathAdd);
    +        if (input.commitish) {
    +          args.push(input.commitish);
    +        }
    +
    +        logger.debug(`Executing command: git ${args.join(" ")}`, {
               ...context,
               operation,
             });
    -        await execAsync(command);
    +        await execFileAsync("git", args);
             // To get the HEAD of the new worktree, we might need another command or parse output if available
             // For simplicity, we'll report success. A more robust solution might `git -C new_worktree_path rev-parse HEAD`
             result = {
    @@ -350,15 +360,17 @@ export async function gitWorktreeLogic(
               input.worktreePath!,
               { allowAbsolute: true, rootDir: targetPath },
             ).sanitizedPath;
    -        command += `remove `;
    -        if (input.force) command += "--force ";
    -        command += `"${sanitizedWorktreePathRemove}"`;
    +        args = ["-C", targetPath, "worktree", "remove"];
    +        if (input.force) {
    +          args.push("--force");
    +        }
    +        args.push(sanitizedWorktreePathRemove);
     
    -        logger.debug(`Executing command: ${command}`, {
    +        logger.debug(`Executing command: git ${args.join(" ")}`, {
               ...context,
               operation,
             });
    -        const { stdout: removeStdout } = await execAsync(command);
    +        const { stdout: removeStdout } = await execFileAsync("git", args);
             result = {
               success: true,
               mode: "remove",
    @@ -379,13 +391,20 @@ export async function gitWorktreeLogic(
               allowAbsolute: true,
               rootDir: targetPath,
             }).sanitizedPath;
    -        command += `move "${sanitizedOldPathMove}" "${sanitizedNewPathMove}"`;
    -
    -        logger.debug(`Executing command: ${command}`, {
    +        args = [
    +          "-C",
    +          targetPath,
    +          "worktree",
    +          "move",
    +          sanitizedOldPathMove,
    +          sanitizedNewPathMove,
    +        ];
    +
    +        logger.debug(`Executing command: git ${args.join(" ")}`, {
               ...context,
               operation,
             });
    -        await execAsync(command);
    +        await execFileAsync("git", args);
             result = {
               success: true,
               mode: "move",
    @@ -396,17 +415,23 @@ export async function gitWorktreeLogic(
             break;
     
           case "prune":
    -        command += "prune ";
    -        if (input.dryRun) command += "--dry-run ";
    -        if (input.verbose) command += "--verbose ";
    -        if (input.expire) command += `--expire "${input.expire}" `;
    +        args = ["-C", targetPath, "worktree", "prune"];
    +        if (input.dryRun) {
    +          args.push("--dry-run");
    +        }
    +        if (input.verbose) {
    +          args.push("--verbose");
    +        }
    +        if (input.expire) {
    +          args.push(`--expire=${input.expire}`);
    +        }
     
    -        logger.debug(`Executing command: ${command}`, {
    +        logger.debug(`Executing command: git ${args.join(" ")}`, {
               ...context,
               operation,
             });
             const { stdout: pruneStdout, stderr: pruneStderr } =
    -          await execAsync(command);
    +          await execFileAsync("git", args);
             // Prune often outputs to stderr even on success for verbose/dry-run
             const pruneMessage =
               pruneStdout.trim() ||
    

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

5

News mentions

0

No linked articles in our index yet.