You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
title: "Codex MCP — User-Project CWD Without Shell-Resync"
3
+
status: draft
4
+
tags:
5
+
- "codex"
6
+
- "multi-host"
7
+
- "plugin"
8
+
---
9
+
10
+
## Idea
11
+
12
+
Restore the archcore MCP server's working directory to the user's project root when invoked from Codex, **without** depending on the inherited `$PWD` env var (which POSIX shells overwrite at startup).
13
+
14
+
Codex spawns plugin MCP servers from the plugin install dir (via `.codex.mcp.json``cwd: "."` rebase). The launcher itself is a `#!/bin/sh` script, so any `$PWD` value Codex inherits from the user's shell is **erased on launcher startup** — POSIX shells (sh, dash, bash --posix) resync `$PWD` to match `getcwd()` if the two diverge. That kills the obvious "follow `$PWD`" workaround.
15
+
16
+
Two viable paths that survive sh's PWD-resync:
17
+
18
+
1.**An explicit env var the shell does NOT resync** — e.g., `ARCHCORE_CWD` (or `CODEX_PROJECT_DIR`, if Codex ever exposes one). The launcher reads it before exec'ing the real CLI:
19
+
20
+
```sh
21
+
if [ -n"${ARCHCORE_CWD:-}" ] && [ -d"$ARCHCORE_CWD" ];then
22
+
cd"$ARCHCORE_CWD"2>/dev/null ||true
23
+
fi
24
+
```
25
+
26
+
Sh's PWD-resync only touches `PWD` and `OLDPWD`. A custom name passes through untouched. The cost: someone (Codex, or the user) has to set it. Until Codex exposes a canonical name, the user opts in per-session.
27
+
28
+
2. **archcore CLI argument** — `archcore --project-root=PATH mcp`. Lets Codex (or a future plugin manifest version) pass the path explicitly. Doesn't require a custom env var, but does require Codex to thread an argument through MCP `args`. As of Codex 0.130.0 there is no env substitution in`args`, so this only helps if upstream adds it (openai/codex#19582).
29
+
30
+
## Value
31
+
32
+
Today, calling any `mcp__archcore__*` tool from Codex creates `.archcore/` documents in`~/.codex/plugins/cache/<marketplace>/archcore/<version>/.archcore/` instead of the user's project. Observed during a Codex `/archcore:bootstrap` session against `test_project/`: two seed docs landed in the plugin cache before the agent gave up and started a manual MCP rooted at the project. ~9m41s of confused wallclock.
33
+
34
+
Closing this gap turns Codex into a first-class Archcore host. Without the fix, the bundled-launcher value-prop of "no global install needed" silently degrades for Codex users — every MCP write pollutes the cache.
35
+
36
+
## Possible Implementation
37
+
38
+
1. **Add `ARCHCORE_CWD` support in `bin/archcore`** — guarded `cd "$ARCHCORE_CWD"` near the top of the launcher (after `SCRIPT_DIR=...` resolution), with `[ -d ]` and `cd ... || true` safety. Unit-tested via the existing Bats pattern (env var → fake `ARCHCORE_BIN` stub → assert stub's `pwd` output).
39
+
2. **Diagnostic mode for env discovery** — `archcore mcp --debug-env` that streams a single JSON-RPC notification listing all `getenv` keys the MCP server sees at startup. Run it once from a real Codex session to identify which env vars Codex actually injects (PLUGIN_ROOT, CODEX_HOME, CODEX_CWD, ...). If a Codex-provided user-project var exists, switch the launcher to read it first; fall back to `ARCHCORE_CWD`.
40
+
3. **Document the contract**in`codex-local-plugin-testing.guide.md` step 8 — add an assertion that an MCP `create_document` call from `mktemp -d` creates files under that tmpdir, **not** under `~/.codex/plugins/cache/...`. Pin the contract with a Bats E2E testif feasible.
41
+
4. **Upstream issue** — file or watch openai/codex#19582 for`${PLUGIN_ROOT}` MCP substitution. Once shipped, drop `cwd: "."`, use `${PLUGIN_ROOT}/bin/archcore` directly, and Codex inherits the user's CWD by default — both legs become unnecessary.
42
+
5. **One-time cleanup utility** — `archcore mcp cleanup-cache` that removes stray `.archcore/` directories that may have landed under `~/.codex/plugins/cache/<marketplace>/archcore/<version>/` before this fix shipped. Opt-in.
43
+
44
+
## Risks and Constraints
45
+
46
+
- **POSIX `$PWD`-resync is non-negotiable.** Verified across `/bin/sh`, `dash`, and `bash --posix` on macOS: each rewrites `$PWD` to match `getcwd()` on every shell startup when they disagree. Any fix that depends on the kernel-inherited `$PWD` is moot in a shell-script launcher.
47
+
- **`ARCHCORE_CWD` is opt-in.** Users who don't set it stay in the broken state. Mitigation: surface a clear "your MCP CWD looks like a plugin cache; set `ARCHCORE_CWD` to your project" warning when archcore CLI detects `getcwd()` matches a plugin-cache pattern (e.g., contains `/plugins/cache/` or `/plugin-install/`). Don't refuse to operate — too disruptive — but make the next step obvious.
48
+
- **Codex env vars may not exist for MCP spawns.** The existing `codex-path-resolution.adr` confirms `${PLUGIN_ROOT}` is injected for hooks but says nothing about MCP. Step 2 (diagnostic mode) is the way to find out. Do this before committing to `ARCHCORE_CWD` as the canonical name.
49
+
- **Cross-host concern.** Claude Code uses `${CLAUDE_PLUGIN_ROOT}/bin/archcore` with no chdir, so `$PWD == $(pwd)` and the issue doesn't surface. Cursor and other hosts may behave like Codex or like Claude — verify each before extending.
50
+
- **Naming.** If we name our env var `ARCHCORE_CWD`, that mirrors `ARCHCORE_BIN` already used by the launcher. Alternative `ARCHCORE_PROJECT_ROOT` is more explicit. Stick with `ARCHCORE_CWD`for brevity unless a survey of other plugins' conventions argues otherwise.
Copy file name to clipboardExpand all lines: skills/bootstrap/SKILL.md
+38-8Lines changed: 38 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -29,16 +29,17 @@ First-time onboarding. Detects repo scale (small / medium / large) and seeds sca
29
29
30
30
## Routing table
31
31
32
-
**Mode routing** — Step 0.5 classifier, evaluated top-to-bottom, first match wins. Precise conditions in `lib/detect-scale.md`.
32
+
**Mode routing** — Step 0.5 classifier, evaluated top-to-bottom, first match wins. The **empty** route is decided earlier in Step 0(b) and short-circuits the classifier entirely. Precise conditions for the rest in `lib/detect-scale.md`.
33
33
34
34
| Signal | Route | Seeded artifacts |
35
35
|---|---|---|
36
+
| No manifest AND no top-level source (Step 0b) | → **empty**| none — acknowledge-only, no placeholder docs |
36
37
|`--mode=X` flag | → forced `X` (detected mode still reported) | per row below |
37
38
|`domain_count ≤ 1` AND `module_count ≤ 15`| → **small**| stack rule, run guide |
38
39
|`domain_count ≤ 2` AND `module_count ≤ 40`| → **medium**| small + entry-point inventory |
39
40
|`domain_count ≥ 3` OR `module_count > 40`| → **large**| medium + top-level map + domain dialog |
40
41
41
-
Each mode additionally runs hotspot capture-candidate proposal (Step 6) and optional agent-file import (Step 8). Medium additionally runs cross-cutting rule candidate (Step 7).
42
+
Each non-empty mode additionally runs hotspot capture-candidate proposal (Step 6) and optional agent-file import (Step 8). Medium additionally runs cross-cutting rule candidate (Step 7). The empty route exits after Step 0.
42
43
43
44
**Follow-up routing** — closing-message hand-offs. Bootstrap surfaces these as todos; MUST NOT auto-invoke.
44
45
@@ -54,15 +55,28 @@ Each mode additionally runs hotspot capture-candidate proposal (Step 6) and opti
54
55
55
56
## Execution
56
57
57
-
### Step -1: Ensure initialization
58
+
### Pre-flight: lazy reading
58
59
59
-
Call `mcp__archcore__init_project()` before any other MCP operation. It is idempotent — safe on an already-initialized project (returns existing settings). It creates `.archcore/` and the settings file if they do not exist.
60
+
Bootstrap MUST give the user fast feedback. The detection catalogs under `skills/bootstrap/lib/` are heavy (≥ 350 lines for scale alone) and they are read **lazily**: do NOT open any `lib/*.md` file until you reach the step that explicitly tells you to read it. Step 0 finishes before any `lib/` file is opened.
61
+
62
+
### Step -1: Initialize and acknowledge (fast)
63
+
64
+
Call `mcp__archcore__init_project()` exactly once. It is idempotent — safe on an already-initialized project (returns existing settings). It creates `.archcore/` and `settings.json` if missing.
65
+
66
+
Immediately after the call, give the user a one-line confirmation so they see something tangible without waiting for any detection:
67
+
68
+
- If the response includes `initialized: true` (created now) — print: *"Archcore initialized at `.archcore/`."*
69
+
- If `already_initialized: true` — print nothing here; the existing knowledge base will speak for itself in Step 0(a).
60
70
61
71
Do NOT ask the user to run `archcore init` in the terminal — `mcp__archcore__init_project` is the correct path in a plugin session.
62
72
63
-
### Step 0: Check state
73
+
### Step 0: Check state and source signal
64
74
65
-
Call `mcp__archcore__list_documents()`. Derive:
75
+
Two cheap probes, in order. Each can short-circuit the whole skill. Neither reads anything under `lib/`.
-`has_stack_rule` — any `rule` whose title contains "stack" in `conventions/`.
68
82
-`has_run_guide` — any `guide` whose title contains "run" or "running" in `onboarding/`.
@@ -76,7 +90,22 @@ If `has_stack_rule` AND `has_run_guide` are both true, reply:
76
90
77
91
Then stop. Per-step idempotency checks (below) handle mode-specific artifacts when the user asks for a selective refresh.
78
92
79
-
Otherwise proceed to Step 0.5.
93
+
#### Step 0(b) — Source-signal gate (empty-repo early exit)
94
+
95
+
Single filesystem probe — one shell call, no catalog reads. Detect whether the repository has any executable shape yet:
96
+
97
+
-**`has_manifest`** — at least one of these exists at the project root (depth ≤ 2 for monorepo workspaces): `package.json`, `pyproject.toml`, `Pipfile`, `requirements.txt`, `Cargo.toml`, `go.mod`, `Gemfile`, `composer.json`, `*.csproj`, `*.fsproj`, `*.vbproj`, `pom.xml`, `build.gradle`, `build.gradle.kts`, `mix.exs`, `Package.swift`.
98
+
-**`has_top_level_source`** — at least one file with a recognizable source extension exists anywhere under the project root, capped at depth 3, excluding `.archcore/`, `.git/`, `node_modules/`, `vendor/`, `dist/`, `build/`, `out/`, `target/`, `coverage/`, `.venv/`, `__pycache__/`, `.next/`, `.turbo/`. Extensions: `.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`, `.cjs`, `.py`, `.rs`, `.go`, `.rb`, `.php`, `.java`, `.kt`, `.kts`, `.swift`, `.cs`, `.fs`, `.ex`, `.exs`, `.scala`, `.clj`, `.cljs`.
99
+
100
+
If BOTH are false, take the **empty** route. Reply with exactly:
101
+
102
+
> Archcore is ready at `.archcore/`. No source code detected yet — nothing to bootstrap.
103
+
>
104
+
> Re-run `/archcore:bootstrap` after the first manifest or source file lands. The SessionStart empty-state nudge will keep pointing here until then.
105
+
106
+
Then stop. **Do NOT** create placeholder documents (no "no stack selected yet" rule, no "no run command yet" guide). They have no practical value, they cost MCP roundtrips and tokens, and they suppress the SessionStart empty-state nudge — which is the user's primary breadcrumb back to bootstrap once code actually exists.
107
+
108
+
Otherwise (`has_manifest` OR `has_top_level_source`), proceed to Step 0.5.
80
109
81
110
### Step 0.5: Detect scale
82
111
@@ -299,8 +328,9 @@ Always end with:
299
328
300
329
Mode-appropriate `.archcore/` seed:
301
330
331
+
- **Empty**: 0 seeded — `.archcore/` and `settings.json` only. Fast acknowledge + early exit. No catalog files read.
All seeds idempotent. Agent-file import is opt-in and previewed.
336
+
All seeds idempotent. Agent-file import is opt-in and previewed. The empty route never creates placeholder documents — it keeps `.archcore/` functionally empty so the SessionStart nudge continues pointing the user back here.
0 commit comments