Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion scripts/claude-companion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { fileURLToPath } from "node:url";

import { parseArgs, splitRawArgumentString } from "./lib/args.mjs";
import {
canonicalPath,
resolveCodexHome,
resolveExpectedPluginDataRoot,
resolvePluginCacheInstallInfo,
Expand Down Expand Up @@ -398,7 +399,9 @@ function pathIsInsideRoot(filePath) {
if (typeof filePath !== "string" || !filePath) {
return false;
}
const relativePath = path.relative(ROOT_DIR, path.resolve(filePath));
// Codex may report the hook through a symlinked CODEX_HOME, e.g. a second
// profile whose plugins dir links to the shared cache.
const relativePath = path.relative(canonicalPath(ROOT_DIR), canonicalPath(filePath));
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
}

Expand Down
2 changes: 1 addition & 1 deletion scripts/lib/codex-paths.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function normalizePathSlashes(value) {
return value.replace(/\\/g, "/");
}

function canonicalPath(candidate) {
export function canonicalPath(candidate) {
try {
return fs.realpathSync.native(candidate);
} catch {
Expand Down
63 changes: 63 additions & 0 deletions tests/integration/claude-companion.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,69 @@ describe("claude-companion integration", () => {
}
});

it("setup recognizes trusted plugin hooks reported through a symlinked plugin root", () => {
const testEnv = createTestEnvironment();
// An alternate CODEX_HOME whose plugins dir symlinks to the shared cache:
// hooks/list reports the alias path, the companion runs from the real path.
const aliasRoot = path.join(testEnv.rootDir, "alias-plugin-root");
fs.symlinkSync(PROJECT_ROOT, aliasRoot, "dir");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use a junction for the Windows directory alias

On native Windows without Developer Mode or CreateSymbolicLink privilege, fs.symlinkSync(..., "dir") throws EPERM before this test enters its try/finally, so the repository-required npm run check now fails on an otherwise supported platform and leaves the temporary test environment behind. Use a junction on win32, as the existing cross-platform symlink tests do.

AGENTS.md reference: AGENTS.md:L3-L3

Useful? React with 👍 / 👎.

const aliasSourcePath = path.join(aliasRoot, "hooks", "hooks.json");
const outsideDir = path.join(testEnv.rootDir, "outside-plugin", "hooks");
fs.mkdirSync(outsideDir, { recursive: true });
fs.copyFileSync(path.join(PROJECT_ROOT, "hooks", "hooks.json"), path.join(outsideDir, "hooks.json"));
const hook = (event, order, extra = {}) => ({
key: `cc@sendbird:hooks/hooks.json:${event}:0:0`,
eventName: event,
handlerType: "command",
matcher: null,
command: "node hook.mjs",
timeoutSec: 600,
statusMessage: null,
sourcePath: aliasSourcePath,
source: "plugin",
pluginId: "cc@sendbird",
displayOrder: order,
enabled: true,
isManaged: false,
currentHash: `sha256:${event}`,
trustStatus: "trusted",
...extra,
});
const hooks = [
hook("session_start", 0),
hook("stop", 1),
hook("user_prompt_submit", 2),
// Same plugin id but a real file outside the plugin root: must stay rejected.
hook("pre_tool_use", 3, {
key: "cc@sendbird:outside:pre_tool_use:0:0",
sourcePath: path.join(outsideDir, "hooks.json"),
trustStatus: "untrusted",
}),
];
const fakeCodex = createFakeCodexAppServer(testEnv, hooks);

try {
const report = runCompanionJson(["setup", "--cwd", testEnv.workspaceDir, "--json"], {
env: {
...testEnv.env,
CC_PLUGIN_CODEX_EXECUTABLE: process.execPath,
CC_PLUGIN_CODEX_APP_SERVER_ARGS_JSON: JSON.stringify([fakeCodex.serverPath]),
CC_PLUGIN_CODEX_FORCE_HOOK_TRUST: "1",
},
});
assert.equal(report.hookTrust.ready, true);
assert.equal(report.hookTrust.found, 3);
assert.equal(report.hookTrust.trusted, 0);
assert.doesNotMatch(report.nextSteps.join("\n"), /trust this plugin's hooks manually/);
const wroteTrust = readJsonLines(fakeCodex.logPath).some(
(r) => r.method === "config/batchWrite" && r.params.edits.some((e) => e.keyPath === "hooks.state")
);
assert.equal(wroteTrust, false, "outside-root hook must not be trusted");
} finally {
cleanupTestEnvironment(testEnv);
}
});

it("setup trusts current native plugin hooks through Codex hooks/list", () => {
const testEnv = createTestEnvironment();
const sourcePath = path.join(PROJECT_ROOT, "hooks", "hooks.json");
Expand Down