App-server: add explicit multi-root customization discovery for AGENTS.md and project hooks
What variant of Codex are you using?
App Server
What feature would you like to see?
Summary
What we are trying to achieve
Context: The microsoft/vscode team is integrating the Codex App Server into VS Code's agent host (see our docs 1 and docs 2)
VS Code supports a multi-root workspace: one editor window can contain several independent folders or repositories. These folders do not need to be nested under one workspace directory, and they do not need to share a meaningful filesystem parent.
For example, a user can create one VS Code workspace containing these two unrelated checkouts:
VS Code workspace: Product Development (virtual grouping only)
├── /Users/alice/code/customer-portal
│ └── AGENTS.md -> use pnpm and run the frontend tests
│
└── /Volumes/company-checkouts/payments-service
└── AGENTS.md -> use Cargo and run the service tests
Product Development is not a directory on disk. The two absolute paths may be separate Git repositories, stored in completely different locations or even on different volumes.
We want one Codex thread to work in both folders and respect the instructions and project hooks owned by each folder. For example, a request may update the customer portal and its payments API in the same turn.
Today the host can start the thread with:
{
"cwd": "/Users/alice/code/customer-portal",
"runtimeWorkspaceRoots": [
"/Users/alice/code/customer-portal",
"/Volumes/company-checkouts/payments-service"
]
}
Codex records both runtime roots, but only the primary cwd contributes native AGENTS.md instructions and project hooks. The payments service is accessible to the thread, but its customization is missing.
What is missing
We need a way to tell app-server:
These independent folders all belong to this thread. Discover customization from each folder, preserve which folder owns each instruction or hook, and keep trust decisions separate for every folder.
This could extend the meaning of runtimeWorkspaceRoots, or use a separate field such as customizationRoots if runtime access and customization discovery should remain separate concepts.
Current behavior
runtimeWorkspaceRootsaccepts and returns every absolute workspace root.- Native
AGENTS.mddiscovery still follows only the primarycwd. hooks/list({ cwds: [...] })can discover hooks from each root independently.- A thread with
cwdset to root A loads root A's project hooks, but not root B's hooks, even when both roots are inruntimeWorkspaceRoots.
An embedding host can work around the instruction part by reading the files itself and passing merged text through developerInstructions. That is enough for basic instruction support, but it duplicates Codex's discovery rules and loses native per-source behavior. It also does not solve project-hook loading.
Minimal reproduction
This script creates two independent temporary workspace folders. Neither folder is inside the other, and there is no generated workspace container above them. It starts a thread but does not start a model turn, so no authentication is required.
Save it as repro.mjs, then run node repro.mjs.
import { spawn } from "node:child_process";
import { createInterface } from "node:readline";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
// These are separate absolute directories, not children of one generated
// workspace directory. This mirrors a VS Code multi-root workspace.
const home = await mkdtemp(join(tmpdir(), "codex-home-"));
const clientApp = await mkdtemp(join(tmpdir(), "customer-portal-"));
const service = await mkdtemp(join(tmpdir(), "payments-service-"));
await Promise.all([
writeFile(join(clientApp, "AGENTS.md"), "CLIENT_APP_INSTRUCTION\n"),
writeFile(join(service, "AGENTS.md"), "SERVICE_INSTRUCTION\n")
]);
const child = spawn(
"npx",
["-y", "@openai/codex@0.147.0", "app-server", "--stdio"],
{
env: { ...process.env, CODEX_HOME: home },
stdio: ["pipe", "pipe", "pipe"]
}
);
let nextId = 0;
const pending = new Map();
createInterface({ input: child.stdout }).on("line", line => {
let message;
try {
message = JSON.parse(line);
} catch {
return;
}
const request = pending.get(message.id);
if (!request) {
return;
}
pending.delete(message.id);
if (message.error) {
request.reject(new Error(JSON.stringify(message.error)));
} else {
request.resolve(message.result);
}
});
child.stderr.on("data", () => {});
function request(method, params) {
const id = ++nextId;
child.stdin.write(JSON.stringify({ method, id, params }) + "\n");
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
});
}
try {
await request("initialize", {
clientInfo: {
name: "multiroot-repro",
title: "Multi-root repro",
version: "1"
},
capabilities: { experimentalApi: true }
});
child.stdin.write(JSON.stringify({ method: "initialized" }) + "\n");
const result = await request("thread/start", {
cwd: clientApp,
runtimeWorkspaceRoots: [clientApp, service],
ephemeral: true
});
console.log(JSON.stringify({
requestedRoots: [clientApp, service],
returnedRuntimeWorkspaceRoots: result.runtimeWorkspaceRoots,
instructionSources: result.instructionSources
}, null, 2));
} finally {
child.kill();
await Promise.all([
rm(home, { recursive: true, force: true }),
rm(clientApp, { recursive: true, force: true }),
rm(service, { recursive: true, force: true })
]);
}
Observed with @openai/codex 0.146.0 and 0.147.0:
{
"requestedRoots": [
"<tmp>/customer-portal-abc123",
"<tmp>/payments-service-def456"
],
"returnedRuntimeWorkspaceRoots": [
"<tmp>/customer-portal-abc123",
"<tmp>/payments-service-def456"
],
"instructionSources": [
"<tmp>/customer-portal-abc123/AGENTS.md"
]
}
The service root is accepted as a runtime workspace root, but its AGENTS.md is not loaded.
The same root-selection difference applies to project hooks: hooks/list({ cwds: [clientApp, service] }) discovers both roots independently, while a thread whose primary cwd is clientApp only loads the client app's project hooks.
Expected behavior
App-server should have an explicit way to discover thread customization from every selected workspace root, even when the roots are independent absolute paths with no shared workspace parent.
The primary cwd should remain the default command-execution directory. Adding a workspace root must not automatically trust that root.
Requested capability
Please either:
- extend thread customization discovery to all supplied
runtimeWorkspaceRoots; or - add an explicit thread-scoped customization-root field.
We need:
- Hierarchical
AGENTS.mddiscovery for every selected root. - Project-hook discovery for every selected root.
- Source attribution that preserves the owning root.
- Independent project-trust handling for each root.
- The primary
cwdto remain unchanged for command execution. - Matching behavior on thread start, resume, and fork.
Secondary roots must not become trusted merely because the client supplied them.
Additional information
_No response_
cc @DonJayamanne
2 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Could you assign this to me