diff --git a/README.md b/README.md index 35eb0f6a..e6ab52ef 100644 --- a/README.md +++ b/README.md @@ -288,6 +288,9 @@ Codex looks for config files in **`~/.codex/`**. model: o4-mini # Default model fullAutoErrorMode: ask-user # or ignore-and-continue notify: true # Enable desktop notifications for responses +safeCommands: + - npm test # Automatically approve npm test + - yarn lint # Automatically approve yarn lint ``` You can also define custom instructions: diff --git a/codex-cli/src/approvals.ts b/codex-cli/src/approvals.ts index 8a670b01..3b3162af 100644 --- a/codex-cli/src/approvals.ts +++ b/codex-cli/src/approvals.ts @@ -4,6 +4,7 @@ import { identify_files_added, identify_files_needed, } from "./utils/agent/apply-patch"; +import { loadConfig } from "./utils/config"; import * as path from "path"; import { parse } from "shell-quote"; @@ -296,6 +297,24 @@ export function isSafeCommand( ): SafeCommandReason | null { const [cmd0, cmd1, cmd2, cmd3] = command; + const config = loadConfig(); + if (config.safeCommands && Array.isArray(config.safeCommands)) { + for (const safe of config.safeCommands) { + // safe: "npm test" → ["npm", "test"] + const safeArr = typeof safe === "string" ? safe.trim().split(/\s+/) : []; + if ( + safeArr.length > 0 && + safeArr.length <= command.length && + safeArr.every((v, i) => v === command[i]) + ) { + return { + reason: "User-defined safe command", + group: "User config", + }; + } + } + } + switch (cmd0) { case "cd": return { diff --git a/codex-cli/src/utils/config.ts b/codex-cli/src/utils/config.ts index be28eebe..45dd9bb5 100644 --- a/codex-cli/src/utils/config.ts +++ b/codex-cli/src/utils/config.ts @@ -56,6 +56,8 @@ export type StoredConfig = { saveHistory?: boolean; sensitivePatterns?: Array; }; + /** User-defined safe commands */ + safeCommands?: Array; }; // Minimal config written on first run. An *empty* model string ensures that @@ -87,6 +89,8 @@ export type AppConfig = { saveHistory: boolean; sensitivePatterns: Array; }; + /** User-defined safe commands */ + safeCommands?: Array; }; // --------------------------------------------------------------------------- @@ -271,6 +275,7 @@ export const loadConfig = ( : DEFAULT_AGENTIC_MODEL), instructions: combinedInstructions, notify: storedConfig.notify === true, + safeCommands: storedConfig.safeCommands ?? [], }; // ----------------------------------------------------------------------- @@ -348,6 +353,13 @@ export const loadConfig = ( }; } + // Load user-defined safe commands + if (Array.isArray(storedConfig.safeCommands)) { + config.safeCommands = storedConfig.safeCommands.map(String); + } else { + config.safeCommands = []; + } + return config; }; @@ -389,6 +401,10 @@ export const saveConfig = ( sensitivePatterns: config.history.sensitivePatterns, }; } + // Save: User-defined safe commands + if (config.safeCommands && config.safeCommands.length > 0) { + configToSave.safeCommands = config.safeCommands; + } if (ext === ".yaml" || ext === ".yml") { writeFileSync(targetPath, dumpYaml(configToSave), "utf-8"); diff --git a/codex-cli/tests/approvals.test.ts b/codex-cli/tests/approvals.test.ts index 7cb0bd3d..a4c08b04 100644 --- a/codex-cli/tests/approvals.test.ts +++ b/codex-cli/tests/approvals.test.ts @@ -1,7 +1,13 @@ import type { SafetyAssessment } from "../src/approvals"; import { canAutoApprove } from "../src/approvals"; -import { describe, test, expect } from "vitest"; +import { describe, test, expect, vi } from "vitest"; + +vi.mock("../src/utils/config", () => ({ + loadConfig: () => ({ + safeCommands: ["npm test", "sl"], + }), +})); describe("canAutoApprove()", () => { const env = { @@ -89,4 +95,27 @@ describe("canAutoApprove()", () => { expect(check(["cargo", "build"])).toEqual({ type: "ask-user" }); }); + + test("commands in safeCommands config should be safe", async () => { + expect(check(["npm", "test"])).toEqual({ + type: "auto-approve", + reason: "User-defined safe command", + group: "User config", + runInSandbox: false, + }); + + expect(check(["sl"])).toEqual({ + type: "auto-approve", + reason: "User-defined safe command", + group: "User config", + runInSandbox: false, + }); + + expect(check(["npm", "test", "--watch"])).toEqual({ + type: "auto-approve", + reason: "User-defined safe command", + group: "User config", + runInSandbox: false, + }); + }); });