Skip to content

Commit 0757e85

Browse files
authored
fix(config-read): read config via native tool, not $() pre-resolution (#942)
1 parent 32a5834 commit 0757e85

7 files changed

Lines changed: 96 additions & 28 deletions

File tree

plugins/compound-engineering/AGENTS.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -274,17 +274,22 @@ When dispatching sub-agents, **omit the `mode` parameter** on the Agent/Task too
274274
275275
Plugin config lives at `.compound-engineering/config.local.yaml` in the repo root. This file is gitignored (machine-local settings), which creates two gotchas:
276276
277-
1. **Path resolution:** Never read the config relative to CWD — the user may invoke a skill from a subdirectory. Always resolve from the repo root. In pre-resolution commands, use `git rev-parse --show-toplevel` to find the root.
277+
1. **Path resolution:** Never read the config relative to CWD — the user may invoke a skill from a subdirectory. Always resolve from the repo root using `git rev-parse --show-toplevel`.
278278
279-
2. **Worktrees:** Gitignored files are per-worktree. A config file created in the main checkout does not exist in worktrees. Use `--show-toplevel` to find the root:
280-
```
281-
!`cat "$(git rev-parse --show-toplevel 2>/dev/null)/.compound-engineering/config.local.yaml" 2>/dev/null || echo '__NO_CONFIG__'`
282-
```
283-
Outside a git repo, `git rev-parse` emits empty and `cat "/.compound-engineering/config.local.yaml"` fails (permission denied or not found, suppressed by `2>/dev/null`), so the `__NO_CONFIG__` sentinel fires. Note: the previous pattern used `(top=$(...); [ -n "$top" ] && cat "$top/...")` with a semicolon to guard the empty-root case, but `;` is rejected by Claude Code's safety checker as `Unhandled node type: ;` (see Pre-resolution exception above) and must not be used in `!` pre-resolution.
279+
2. **Worktrees:** Gitignored files are per-worktree. A config file created in the main checkout does not exist in worktrees. `--show-toplevel` returns the current worktree path, so config from the main checkout is not found from a worktree. This is acceptable — config is optional and users who work from worktrees can add a config file there.
284280
285-
Note: in a worktree, `--show-toplevel` returns the worktree path, so config from the main checkout will not be found. This is acceptable — config is optional and users who work from worktrees can add a config file there. A previous pattern used `git-common-dir` with `${common%/.git}` to derive the main repo root as a fallback, but bash parameter expansion operators are rejected as "Contains expansion" (see Pre-resolution exception above), so that approach is no longer viable without a script.
281+
**Blessed pattern — pre-resolve only the repo root, then read the file in the skill body:**
282+
```
283+
!`git rev-parse --show-toplevel 2>/dev/null || true`
284+
```
285+
In the skill body: if the pre-resolved line is an absolute path, use it as `<repo-root>`; if it is empty or still shows a backtick command string, resolve `<repo-root>` at runtime by running `git rev-parse --show-toplevel` with the shell tool, then read `<repo-root>/.compound-engineering/config.local.yaml` with the native file-read tool (Read in Claude Code, read_file in Codex). The runtime fallback is load-bearing: `!` pre-resolution is Claude-Code-only and is **not** translated by the converters (`transformContentForCodex` and the other target writers copy `!`cmd`` through as literal text), so on Codex/Gemini/Pi/OpenCode the line is unresolved and the agent must derive the root itself — otherwise config is silently ignored on every non-Claude platform. Only when the root cannot be resolved (not a git repo) or the file is missing is it "no config" → fall through to defaults; never fail or block on missing config. (This mirrors the `ce-sessions` pre-resolution fallback.)
286+
287+
**Do NOT read the config contents inside the `!` pre-resolution itself.** Two earlier forms are now rejected and enforced against by `tests/skill-shell-safety.test.ts`:
288+
289+
- `` !`cat "$(git rev-parse --show-toplevel 2>/dev/null)/.compound-engineering/config.local.yaml" 2>/dev/null || echo '__NO_CONFIG__'` `` aborts skill load on environments whose permission checker rejects `$()` command substitution (reported on Windows / CC 2.1.63 in issue #920).
290+
- The `(top=$(...); cat "$top/...")` subshell variant is worse: `;` is rejected as `Unhandled node type: ;` (issue #758), even inside a subshell.
286291
287-
If neither path has the file, fall through to defaults — never fail or block on missing config.
292+
The blessed pattern sidesteps both: a bare `git rev-parse` first token has no `$()`, no `;`, and matches common allow rules, while the config contents are read with a native tool at runtime (no skill-load checker involved). A previous worktree fallback that derived the main repo root via `git-common-dir` with `${common%/.git}` is also unavailable — parameter expansion operators are rejected as "Contains expansion" (see Pre-resolution exception above).
288293
289294
### Quick Validation Command
290295

plugins/compound-engineering/skills/ce-brainstorm/SKILL.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,17 @@ Do not proceed until you have a feature description from the user.
6767

6868
Determine `OUTPUT_FORMAT` before any other phase fires. Output mode is **exclusive** — the requirements doc is written as either markdown (`.md`) OR HTML (`.html`), never both. Precedence: CLI arg > config > default (`md`), with a hard pipeline-mode override.
6969

70-
**Read config (pre-resolved at skill load):**
71-
!`cat "$(git rev-parse --show-toplevel 2>/dev/null)/.compound-engineering/config.local.yaml" 2>/dev/null || echo '__NO_CONFIG__'`
70+
**Read config.** The repo root is pre-resolved at skill load:
71+
!`git rev-parse --show-toplevel 2>/dev/null || true`
72+
73+
If the line above is an absolute path, use it as `<repo-root>`. If it is empty or still shows a backtick command string (a non-Claude harness that did not run the pre-resolution), resolve `<repo-root>` at runtime by running `git rev-parse --show-toplevel` with the shell tool. Then read `<repo-root>/.compound-engineering/config.local.yaml` with the native file-read tool. If the root cannot be resolved (not a git repo) or the file does not exist, fall through to the defaults below.
7274

7375
Resolution steps:
7476

7577
1. **CLI arg.** Scan `$ARGUMENTS` for a token starting with the literal prefix `output:`. If found, strip it from arguments before treating the remainder as the feature description, and match its value case-insensitively against `md` and `html`.
7678
- `output:` alone (no value) → no-op, fall through to step 2.
7779
- `output:<unknown>` (e.g., `output:pdf`) → drop the token, fall through to step 2, and remember to emit a one-line note above the post-generation menu after final resolution: `Ignored unknown output: value '<value>' — using <resolved_format> instead.` where `<resolved_format>` is the value `OUTPUT_FORMAT` actually resolved to after steps 2-4. Do not hardcode `md` in the note — that misleads users when config has set HTML.
78-
2. **Config.** If step 1 did not resolve and the pre-resolved YAML above has an **active (non-commented)** `brainstorm_output:` key whose value matches `md` or `html` (case-insensitive), use it. Missing, invalid, or commented values fall through silently. Critical: lines starting with `#` are YAML comments and must be ignored — the shipped config template includes commented examples like `# brainstorm_output: html` to document the option, and matching those as active settings would silently force HTML mode on every run without the user having opted in.
80+
2. **Config.** If step 1 did not resolve and the config file read above has an **active (non-commented)** `brainstorm_output:` key whose value matches `md` or `html` (case-insensitive), use it. Missing, invalid, or commented values fall through silently. Critical: lines starting with `#` are YAML comments and must be ignored — the shipped config template includes commented examples like `# brainstorm_output: html` to document the option, and matching those as active settings would silently force HTML mode on every run without the user having opted in.
7981
3. **Default.** Otherwise `OUTPUT_FORMAT=md`.
8082
4. **Pipeline override.** When invoked from LFG or any `disable-model-invocation` context, force `OUTPUT_FORMAT=md` regardless of steps 1-3. Downstream consumers (`ce-plan`, `ce-work`) parse markdown reliably; HTML in pipeline runs is unnecessary friction.
8183

plugins/compound-engineering/skills/ce-ideate/SKILL.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,17 @@ Determine `OUTPUT_FORMAT` for the ideation artifact this run might persist. Outp
6767

6868
Unlike `ce-plan` and `ce-brainstorm` (which default to `md`), ce-ideate defaults to **`html`** — ideation artifacts are read mainly by humans weighing candidate directions, and a rich self-contained HTML file (with illustrative diagrams for the top candidates) makes the ideas easier to approach.
6969

70-
**Read config (pre-resolved at skill load):**
71-
!`cat "$(git rev-parse --show-toplevel 2>/dev/null)/.compound-engineering/config.local.yaml" 2>/dev/null || echo '__NO_CONFIG__'`
70+
**Read config.** The repo root is pre-resolved at skill load:
71+
!`git rev-parse --show-toplevel 2>/dev/null || true`
72+
73+
If the line above is an absolute path, use it as `<repo-root>`. If it is empty or still shows a backtick command string (a non-Claude harness that did not run the pre-resolution), resolve `<repo-root>` at runtime by running `git rev-parse --show-toplevel` with the shell tool. Then read `<repo-root>/.compound-engineering/config.local.yaml` with the native file-read tool. If the root cannot be resolved (not a git repo) or the file does not exist, fall through to the defaults below.
7274

7375
Resolution steps:
7476

7577
1. **CLI arg.** Scan `$ARGUMENTS` for a token starting with the literal prefix `output:`. If found, strip it from arguments before treating the remainder as the focus hint, and match its value case-insensitively against `md` and `html`.
7678
- `output:` alone (no value) → no-op, fall through to step 2.
7779
- `output:<unknown>` (e.g., `output:pdf`) → drop the token, fall through to step 2, and remember to emit a one-line note above the post-ideation menu after final resolution: `Ignored unknown output: value '<value>' — using <resolved_format> instead.` where `<resolved_format>` is the value `OUTPUT_FORMAT` actually resolved to after steps 2-4. Do not hardcode a format in the note — that misleads users when config or the default differs from what you assume.
78-
2. **Config.** If step 1 did not resolve and the pre-resolved YAML above has an **active (non-commented)** `ideate_output:` key whose value matches `md` or `html` (case-insensitive), use it. Missing, invalid, or commented values fall through silently. Critical: lines starting with `#` are YAML comments and must be ignored — the shipped config template includes a commented example like `# ideate_output: md` to document the option, and matching that as an active setting would silently override the default on every run without the user having opted in.
80+
2. **Config.** If step 1 did not resolve and the config file read above has an **active (non-commented)** `ideate_output:` key whose value matches `md` or `html` (case-insensitive), use it. Missing, invalid, or commented values fall through silently. Critical: lines starting with `#` are YAML comments and must be ignored — the shipped config template includes a commented example like `# ideate_output: md` to document the option, and matching that as an active setting would silently override the default on every run without the user having opted in.
7981
3. **Default.** Otherwise `OUTPUT_FORMAT=html`.
8082
4. **Pipeline override.** When invoked from any pipeline or `disable-model-invocation` context, force `OUTPUT_FORMAT=md` regardless of steps 1-3 — automated downstream consumers parse markdown reliably and HTML in pipeline runs is unnecessary friction.
8183

plugins/compound-engineering/skills/ce-plan/SKILL.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,17 @@ A plan is ready when an implementer can start confidently without needing the pl
6363

6464
Determine `OUTPUT_FORMAT` before any other phase fires. Output mode is **exclusive** — the plan is written as either markdown (`.md`) OR HTML (`.html`), never both. Precedence: CLI arg > config > default (`md`), with a hard pipeline-mode override.
6565

66-
**Read config (pre-resolved at skill load):**
67-
!`cat "$(git rev-parse --show-toplevel 2>/dev/null)/.compound-engineering/config.local.yaml" 2>/dev/null || echo '__NO_CONFIG__'`
66+
**Read config.** The repo root is pre-resolved at skill load:
67+
!`git rev-parse --show-toplevel 2>/dev/null || true`
68+
69+
If the line above is an absolute path, use it as `<repo-root>`. If it is empty or still shows a backtick command string (a non-Claude harness that did not run the pre-resolution), resolve `<repo-root>` at runtime by running `git rev-parse --show-toplevel` with the shell tool. Then read `<repo-root>/.compound-engineering/config.local.yaml` with the native file-read tool. If the root cannot be resolved (not a git repo) or the file does not exist, fall through to the defaults below.
6870

6971
Resolution steps:
7072

7173
1. **CLI arg.** Scan `$ARGUMENTS` for a token starting with the literal prefix `output:`. If found, strip it from arguments before treating the remainder as the feature description, and match its value case-insensitively against `md` and `html`.
7274
- `output:` alone (no value) → no-op, fall through to step 2.
7375
- `output:<unknown>` (e.g., `output:pdf`) → drop the token, fall through to step 2, and remember to emit a one-line note above the post-generation menu after final resolution: `Ignored unknown output: value '<value>' — using <resolved_format> instead.` where `<resolved_format>` is the value `OUTPUT_FORMAT` actually resolved to after steps 2-4. Do not hardcode `md` in the note — that misleads users when config has set HTML.
74-
2. **Config.** If step 1 did not resolve and the pre-resolved YAML above has an **active (non-commented)** `plan_output:` key whose value matches `md` or `html` (case-insensitive), use it. Missing, invalid, or commented values fall through silently. Critical: lines starting with `#` are YAML comments and must be ignored — the shipped config template includes commented examples like `# plan_output: html` to document the option, and matching those as active settings would silently force HTML mode on every run without the user having opted in.
76+
2. **Config.** If step 1 did not resolve and the config file read above has an **active (non-commented)** `plan_output:` key whose value matches `md` or `html` (case-insensitive), use it. Missing, invalid, or commented values fall through silently. Critical: lines starting with `#` are YAML comments and must be ignored — the shipped config template includes commented examples like `# plan_output: html` to document the option, and matching those as active settings would silently force HTML mode on every run without the user having opted in.
7577
3. **Default.** Otherwise `OUTPUT_FORMAT=md`.
7678
4. **Pipeline override.** When invoked from LFG or any `disable-model-invocation` context, force `OUTPUT_FORMAT=md` regardless of steps 1-3. `ce-work` and other automated downstream consumers parse markdown reliably; HTML in pipeline runs is unnecessary friction.
7779

plugins/compound-engineering/skills/ce-product-pulse/SKILL.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,10 @@ Apply a **15-minute trailing buffer** to the window's upper bound. Many analytic
5151

5252
### Phase 0: Route by Config State
5353

54-
**Config (pre-resolved):**
55-
!`(top=$(git rev-parse --show-toplevel 2>/dev/null); [ -n "$top" ] && cat "$top/.compound-engineering/config.local.yaml" 2>/dev/null) || echo '__NO_CONFIG__'`
54+
**Read config.** The repo root is pre-resolved at skill load:
55+
!`git rev-parse --show-toplevel 2>/dev/null || true`
5656

57-
If the block above contains YAML key-value pairs, extract values for the `pulse_*` keys listed under "Config keys" below.
58-
If it shows `__NO_CONFIG__`, the file does not exist — treat this as a first run.
59-
If it shows an unresolved command string, read `.compound-engineering/config.local.yaml` from the repo root using the native file-read tool (e.g., Read in Claude Code, read_file in Codex). If the file does not exist, treat as first run.
57+
If the line above is an absolute path, use it as `<repo-root>`. If it is empty or still shows a backtick command string (a non-Claude harness that did not run the pre-resolution), resolve `<repo-root>` at runtime by running `git rev-parse --show-toplevel` with the shell tool. Then read `<repo-root>/.compound-engineering/config.local.yaml` with the native file-read tool (e.g., Read in Claude Code, read_file in Codex). If the root cannot be resolved or the file does not exist, treat this as a first run. Otherwise extract values for the `pulse_*` keys listed under "Config keys" below.
6058

6159
**Config keys:**
6260
- `pulse_product_name` -- string, used in report titles. Required for routing: if unset, skill is unconfigured.

plugins/compound-engineering/skills/ce-work-beta/SKILL.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,10 @@ After extracting tokens from arguments, resolve the delegation state using this
4242
2. **Config file** -- extract settings from the config block below. Value `codex` for `work_delegate` activates delegation; `false` deactivates.
4343
3. **Hard default** -- `false` (delegation off)
4444

45-
**Config (pre-resolved):**
46-
!`cat "$(git rev-parse --show-toplevel 2>/dev/null)/.compound-engineering/config.local.yaml" 2>/dev/null || echo '__NO_CONFIG__'`
45+
**Read config.** The repo root is pre-resolved at skill load:
46+
!`git rev-parse --show-toplevel 2>/dev/null || true`
4747

48-
If the block above contains YAML key-value pairs, extract values for the keys listed below.
49-
If it shows `__NO_CONFIG__`, the file does not exist — all settings fall through to defaults.
50-
If it shows an unresolved command string, read `.compound-engineering/config.local.yaml` from the repo root using the native file-read tool (e.g., Read in Claude Code, read_file in Codex). If the file does not exist, all settings fall through to defaults.
48+
If the line above is an absolute path, use it as `<repo-root>`. If it is empty or still shows a backtick command string (a non-Claude harness that did not run the pre-resolution), resolve `<repo-root>` at runtime by running `git rev-parse --show-toplevel` with the shell tool. Then read `<repo-root>/.compound-engineering/config.local.yaml` with the native file-read tool (e.g., Read in Claude Code, read_file in Codex). If the root cannot be resolved or the file does not exist, all settings fall through to defaults. Otherwise extract values for the keys listed below.
5149

5250
If any setting has an unrecognized value, fall through to the hard default for that setting. For optional settings without a hard default (`work_delegate_model`, `work_delegate_effort`), an unrecognized or unparseable value resolves to **unset** — the corresponding flag is omitted from the `codex exec` invocation so Codex resolves from `~/.codex/config.toml`. Never substitute an invalid value into the CLI flags.
5351

0 commit comments

Comments
 (0)