App-server: add safe thread-scoped registration for host-provided hooks

Open 💬 1 comment Opened Aug 13, 2026 by Giuspepe

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)

An application embedding Codex app-server can collect hooks from workspace files, plugins, or extension contributions. We want to attach the selected hooks to one Codex thread and have only those exact hooks run.

For example, a workspace may define this hook:

After Codex edits a file, run the project formatter.

The host can send that hook through thread/start.config.hooks, but Codex treats the injected hook as untrusted and does not run it.

The only available workaround is bypass_hook_trust: true. That trusts every hook affecting the thread, not only the hook supplied by the host, so it is too broad for a production integration.

What is missing

We need a safe way for an embedding application to say:

Run these exact host-selected hooks for this thread. Do not change the trust status of any other hooks.

This could be a typed thread-level hooks field, a scoped approval API, or another mechanism. The important requirement is that trust applies only to the supplied hook definitions and only to the target thread.

Current behavior

  • thread/start.config.hooks accepts the hook configuration.
  • The hook is classified as a session-provided, untrusted hook.
  • The hook does not execute.
  • hooks/list cannot inspect the effective hooks injected into a particular thread.
  • bypass_hook_trust: true makes the hook run, but disables trust checking for all hooks affecting that thread.

Minimal reproduction

This script requires Node.js and macOS or Linux. It uses a local mock model endpoint, so it does not require a Codex login or make an external model request.

Save it as repro.mjs, then run node repro.mjs.

import { spawn } from "node:child_process";
import { createServer } from "node:http";
import { createInterface } from "node:readline";
import { access, mkdir, mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";

const tmp = await mkdtemp(join(tmpdir(), "codex-hook-repro-"));
const mockModel = createServer((_request, response) => {
  response.writeHead(400, { "content-type": "application/json" });
  response.end(JSON.stringify({ error: { message: "intentional repro stop" } }));
});
await new Promise(resolve => mockModel.listen(0, "127.0.0.1", resolve));
const port = mockModel.address().port;

async function exists(path) {
  try {
    await access(path);
    return true;
  } catch {
    return false;
  }
}

async function run(label, bypassHookTrust) {
  const home = join(tmp, `home-${label}`);
  const cwd = join(tmp, `cwd-${label}`);
  const marker = join(tmp, `${label}.txt`);
  await Promise.all([mkdir(home), mkdir(cwd)]);

  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 });
    });
  }

  await request("initialize", {
    clientInfo: { name: "hook-repro", title: "Hook repro", version: "1" }
  });
  child.stdin.write(JSON.stringify({ method: "initialized" }) + "\n");

  const config = {
    model_provider: "repro",
    model_providers: {
      repro: {
        name: "repro",
        base_url: `http://127.0.0.1:${port}/v1`,
        wire_api: "responses",
        requires_openai_auth: false
      }
    },
    hooks: {
      SessionStart: [{
        hooks: [{
          type: "command",
          command: `printf ${label} > '${marker}'`
        }]
      }]
    },
    ...(bypassHookTrust ? { bypass_hook_trust: true } : {})
  };

  const started = await request("thread/start", {
    cwd,
    model: "repro-model",
    config,
    ephemeral: true
  });
  await request("turn/start", {
    threadId: started.thread.id,
    input: [{ type: "text", text: "hello" }]
  });

  for (let i = 0; i < 30 && !(await exists(marker)); i++) {
    await new Promise(resolve => setTimeout(resolve, 50));
  }

  const ran = await exists(marker);
  child.kill();
  return ran;
}

try {
  console.log(JSON.stringify({
    withoutBypass: await run("without-bypass", false),
    withBypass: await run("with-bypass", true)
  }, null, 2));
} finally {
  mockModel.close();
  await rm(tmp, { recursive: true, force: true });
}

Observed with @openai/codex 0.146.0 and 0.147.0:

{
  "withoutBypass": false,
  "withBypass": true
}

Expected behavior

The host should be able to register and trust only the hook definitions it selected for this thread. Unrelated project, user, or plugin hooks should keep their existing trust state.

Requested capability

Please add a typed, thread-scoped registration surface for host-provided hooks, or equivalent scoped trust support. The exact API shape is flexible, but we need:

  • Scoped trust for the exact host-provided hook definitions.
  • Stable hook IDs and source/provenance information.
  • Thread-aware listing that includes trust status and validation errors.
  • Clear behavior when hooks are updated on a loaded thread.
  • Support for event, matcher, command, cwd, environment, timeout, and platform-specific command fields.
  • Consistent behavior for thread start, resume, and fork.

Native untrusted project, user, and plugin hooks must remain untrusted. Managed-hook restrictions must still take precedence.

The solution should not require an embedding host to reproduce Codex's private hook hash algorithm or set bypass_hook_trust.

Additional information

_No response_

cc @DonJayamanne

View original on GitHub ↗

1 Comment

SankeerthNara · 14 days ago

Could you assign this to me