Skip to content

Installer: install for one project with --project or --local - #27

Open
otavio wants to merge 6 commits into
chhoumann:mainfrom
otavio:feat/project-install-support
Open

otavio wants to merge 6 commits into
chhoumann:mainfrom
otavio:feat/project-install-support

Conversation

@otavio

@otavio otavio commented Oct 17, 2026

Copy link
Copy Markdown

Why

The installer only installs globally, in every Claude Code and Codex home T3 can run, so t3-pstack is on in every session. Users who want it in one repository, or who want to share it with a team through the repository, had no way to do that.

What changed

install, update, and doctor take --project <dir> or --local <dir>. They still run against every home the installer finds.

  • Claude Code installs at that scope, run from the project. The marketplace and the enabled plugin land in .claude/settings.json (--project, shared) or .claude/settings.local.json (--local). Each home still registers the marketplace and keeps its own install record. Doctor reads the project's other settings file too, so a foreign marketplace declared there, or a local file that turns the plugin off, fails it.
  • Codex has no plugin scopes. A session reads a project's .codex/config.toml only once the home trusts the project, and the app-server refuses writes to any file but the home's config. So the installer:
    • trusts the project in each home ([projects."<dir>"] trust_level = "trusted");
    • appends [plugins."t3-pstack@t3-pstack"] enabled = true to the project's config, and refuses to edit a file that already names the plugin or has a plugins value;
    • leaves the plugin disabled in the home unless the home already had it enabled, since codex plugin add enables it there, so it loads only in projects that enable it;
    • checks the marketplace source in the project's .codex layers too, read before the project is trusted, so a marketplace the project names from another source gets neither the project's trust nor the hook's.
  • The fake bun in tests/ci-forks-step.test.mjs uses #!/usr/bin/env bash, because /bin/bash does not exist on systems such as NixOS.

The two refactor commits come first and change no behavior: runCli takes a cwd and marketplaceCheck a fix hint, and Claude Code's read() returns the settings path and the record it uses.

Verified

  • bun test tests/ --timeout 30000: 546 pass, 0 fail, at each commit in a clean checkout, with bun tools/generate.mjs --check passing.
  • T3_PSTACK_E2E=1 bun test --timeout 300000 tests/install-e2e.test.mjs with claude 2.1.301 and codex-cli 0.163.0: 19 pass, 0 fail. Before the end-to-end commit, the 12 existing tests pass with the feature in place. The new suite checks that the Codex session-start hook runs in the project's sessions and not outside it, that a rerun changes nothing, and that a later global install coexists.
  • Reviewed over five rounds by Codex (GPT-6.1-Sol, high reasoning), which approved after 15 findings were fixed.

The fake bun the fork-check test puts on PATH started with #!/bin/bash, which does not exist on
systems such as NixOS, so the test failed there. env finds bash on PATH wherever it lives.
…ce check a fix

A project install has to run `claude plugin` from the project, so Claude Code writes that
project's settings, and its checks have to name the rerun that fixes them. Both default to what
they did before, so nothing changes for the global install.
…h and record

installClaude and doctorClaude reached for settings.json and the user-scope record by name. read()
now returns the settings file's `path` and the `record` the install uses, so a project install can
point both at the project's settings and its own record. Every value is the same as before.
`install`, `update`, and `doctor` take `--project <dir>` or `--local <dir>` to install the plugin
for one project instead of every session, run against every home as before.

Claude Code installs at that scope from the project, so the marketplace and the enabled plugin land
in the project's .claude/settings.json, which the team shares, or .claude/settings.local.json. Each
home still registers the marketplace and records its own install. Doctor reads the project's other
settings file too, so a foreign marketplace or a local override that turns the plugin off fails it.

Codex has no plugin scopes. Its sessions read a project's .codex/config.toml only once the home
trusts the project, and its app-server writes only the home's config. So the installer trusts the
project in each home and appends the plugin's table to the project's config. It refuses to edit a
file that already names the plugin or has a `plugins` value. A home that did not already have the
plugin enabled keeps it disabled outside the projects that enable it. The marketplace source check
also reads the project's .codex layers, before the project is trusted, so a marketplace the project
names from another source gets neither the project's trust nor the hook's.
With the real CLIs: the plugin and its hook load in the project's Codex sessions and nowhere else,
a rerun changes nothing, --local keeps the project's other Codex settings, every fresh home
registers the marketplace, a project-only marketplace still gets the home its own, a marketplace
the project names from another source is trusted nowhere, and a later global install coexists.
README says what --project and --local write for each host, and why Codex trusts the project.
CONTEXT's installer entry names the two flags.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant