openai / openai/codex

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

Open
#38,372 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

app-server config enhancement hooks session
Dominant language
Rust
Stars
125k
Forks
19.5k
PR merge metrics
PR metrics pending

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by running repro.mjs and compare thread/start with the returned instructionSources and runtimeWorkspaceRoots. Review hooks/list({ cwds: [...] }) alongside thread start, resume, and fork behavior. Done means selected independent roots discover their AGENTS.md files and project hooks with owning-root attribution and separate trust handling, while cwd remains the command directory.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, rust
Domain
api, backend, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.