App-server: add explicit multi-root customization discovery for AGENTS.md and project hooks

Open 💬 2 comments Opened Aug 13, 2026 by Giuspepe
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

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

  • runtimeWorkspaceRoots accepts and returns every absolute workspace root.
  • Native AGENTS.md discovery still follows only the primary cwd.
  • hooks/list({ cwds: [...] }) can discover hooks from each root independently.
  • A thread with cwd set to root A loads root A's project hooks, but not root B's hooks, even when both roots are in runtimeWorkspaceRoots.

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:

  1. extend thread customization discovery to all supplied runtimeWorkspaceRoots; or
  2. add an explicit thread-scoped customization-root field.

We need:

  • Hierarchical AGENTS.md discovery 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 cwd to 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

View original on GitHub ↗

2 Comments

github-actions[bot] contributor · 14 days ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #38065

Powered by Codex Action

SankeerthNara · 14 days ago

Could you assign this to me