Skip to content

Commit ce301f4

Browse files
derek-palmerclaude
andauthored
security: fix H1-H3 + M1-M7 + L1-L9 from security review (#20)
* feat: add --prompt-only flag and skill-mode auto-detect to forerunner generate In skill mode (Claude Code, Codex, Gemini CLI, etc.) the host agent IS the model — calling forerunner generate should output the assembled prompt bundle for the agent to process, not make a provider API call. Changes: - `forerunner generate --prompt-only <task>`: outputs the assembled prompt bundle to stdout and exits; no API key or provider needed. This is the correct command for skill-mode use in all SKILL.md Execute sections. - Auto-detect skill context: when no explicit provider is configured, no API key is set, and Ollama is not running, cmd_generate now emits the prompt bundle directly (exit 0) instead of erroring. On a TTY, adds a brief stderr hint; on piped stdout, emits cleanly with no extra output. - `forerunner doctor`: "no API key" is now [ok] when a managed skill destination is installed (skill mode active) rather than [warn]. - All 26 per-task SKILL.md Execute sections updated: `forerunner doc <task>` → `forerunner generate --prompt-only <task>` - Canonical skill body (agent/codeforerunner.skill.md) updated with skill-mode explanation and synced to both distribution copies. - 194 tests pass (7 updated to reflect new behavior). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(doctor): always warn on missing provider key; hint --prompt-only in skill mode doctor was returning [ok] when skill mode active + provider configured + key missing. forerunner generate (without --prompt-only) still refuses to run in that case, so the ok was misleading. Now always emits warn; appends a --prompt-only hint when skill mode is detected so the user knows the key-free path exists. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * security: fix H1-H3 + M1-M7 + L1-L9 from security review H1: gate _load_script_module behind --run-scripts flag (off by default) H2: pin npm/GitHub fallback to v0.4.1 in install.sh and install.ps1 H3: document Google API key-in-URL risk with comment in google.py M1: fix enabled_rules YAML placement (root→tasks.check) in config and dogfood yaml M2: validate MCP tools/call name against path traversal + resolve check M3: eliminate thread-unsafe sys.stdout swap in cmd_generate; use _get_bundle() M4: add timeout=120 to all provider urlopen calls M5: fix dead any_error flag in install_all_skills; propagate write failures M6: wrap int() in _to_int() raising ConfigError with descriptive message M7: validate OLLAMA_HOST scheme and reject 169.254.* metadata endpoints L1: replace assert in overlay() with RuntimeError L2: remove unused output_dir/context_max_files/context_max_lines_per_file fields L3: use UUID suffix in _load_script_module to prevent stale sys.modules cache L4: use line-by-line read in _description_for instead of loading full file L5: use direct dict access in format_report (drop redundant .get()) L6: enforce initialize-first in MCP server; return -32002 if skipped L7: replace new RegExp(needle) with includes() in vscodeExtPresent/cursorExtPresent L8: add test_generate_stream_flag_yields_chunks covering --stream branch L9: limit find_prompts_root walk to 10 parent directories Bump version to 0.4.2. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 174cde0 commit ce301f4

49 files changed

Lines changed: 411 additions & 157 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,33 @@ All notable changes to `codeforerunner` are documented here.
55
The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
66
this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Security
11+
12+
- **H1** — `forerunner doctor` no longer executes Python scripts from the target repo by default. Script-based checks (`skill-body-parity`, `codex-marketplace`) now emit `[warn] skipping script validation` unless `--run-scripts` is passed explicitly. (`doctor.py`, `cli.py`)
13+
- **H2** — `install.sh` and `install.ps1` now pin the npm package to `codeforerunner@0.4.1` and the GitHub fallback to `github:derek-palmer/codeforerunner#v0.4.1`, preventing arbitrary code execution in `curl|bash` one-liner installs.
14+
- **H3** — Added comment in `providers/google.py` documenting that the API key appears in the URL query string (Google REST API requirement; key may appear in proxy logs).
15+
- **M2** — MCP server `tools/call` validates that the tool `name` contains no path separators or `..` components, and checks the resolved path is inside `prompts/tasks/` before executing. (`mcp_server.py`)
16+
- **M7** — `OllamaProvider` and `is_available` now validate the base URL scheme (`http`/`https`) and reject `169.254.*` link-local addresses to prevent SSRF via `OLLAMA_HOST`. (`providers/ollama.py`)
17+
18+
### Fixed
19+
20+
- **M1** — `forerunner.config.yaml` and `_STARTER_CONFIG` in `doctor.py` had `enabled_rules` at the YAML root instead of under `tasks.check`. This was a functional bug causing the field to be silently ignored. Both the dogfood config and the `--fix` template are corrected.
21+
- **M3** — `cmd_generate` no longer swaps `sys.stdout` globally (thread-unsafe). Bundle resolution now goes through `_get_bundle()` which returns a string directly. (`cli.py`)
22+
- **M4** — All provider `urlopen` calls now pass `timeout=120` to prevent indefinite hangs. (`providers/anthropic.py`, `openai.py`, `google.py`, `ollama.py`)
23+
- **M5** — `install_all_skills` now correctly sets `any_error = True` when a source file is missing or a write fails, so install failures propagate to the return code. (`installer.py`)
24+
- **M6** — `int()` calls in `config.py` are now wrapped in `_to_int()` which raises `ConfigError` with a descriptive message instead of bare `ValueError`. (`config.py`)
25+
- **L1** — Replaced `assert span is not None` in `overlay()` with `RuntimeError`. (`installer.py`)
26+
- **L2** — Removed unused `output_dir`, `context_max_files`, `context_max_lines_per_file` fields from `ForerunnerConfig` and the YAML parser. (`config.py`)
27+
- **L3** — `_load_script_module` now uses a UUID-suffixed module name to prevent stale cached modules on repeated calls. (`doctor.py`)
28+
- **L4** — `_description_for` in MCP server now reads line-by-line instead of loading the entire file into memory. (`mcp_server.py`)
29+
- **L5** — `format_report` uses direct dict access (`counts['ok']`) instead of redundant `.get()` calls. (`doctor.py`)
30+
- **L6** — MCP server now enforces `initialize` as the first call; `tools/list` and `tools/call` before `initialize` return JSON-RPC error `-32002 Server not initialized`. (`mcp_server.py`)
31+
- **L7** — `vscodeExtPresent` and `cursorExtPresent` in `bin/install.js` now use `includes()` instead of `new RegExp(needle)`, preventing extension ID metacharacters from being interpreted as regex. (`bin/install.js`)
32+
- **L8** — Added unit test `test_generate_stream_flag_yields_chunks` covering the `--stream` branch of `cmd_generate`. (`tests/test_cli.py`)
33+
- **L9** — `find_prompts_root` now stops walking after 10 parent directories to avoid traversing to the filesystem root. (`bundle.py`)
34+
835
## [0.4.1] — 2026-05-24
936

1037
### Added

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ forerunner check # run any time or as pre-commit hook
148148
## GitHub Action
149149

150150
```yaml
151-
- uses: derek-palmer/codeforerunner@v0.4.1
151+
- uses: derek-palmer/codeforerunner@v0.4.2
152152
```
153153
154154
No-op when `forerunner.config.yaml` is absent.

‎agent/codeforerunner.skill.md‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ description: Routes a coding agent through the tracked codeforerunner prompt pac
77

88
The authoritative canonical source for this skill body is `agent/codeforerunner.skill.md` (see the `SPEC.md` I.agent-skill entry). Distribution copies are not independent sources; maintainers should edit the canonical file, then sync the post-frontmatter body into Codex and Claude copies. Downstream packages must preserve this post-frontmatter Markdown content verbatim (`SPEC.md` V10); per-agent YAML frontmatter may differ, but the body cannot.
99

10-
This skill does not bundle a runtime. It routes the host agent into the codeforerunner prompt pack (`src/codeforerunner/prompts/` in the source repo, or retrieved via `forerunner doc <task>` from the installed CLI). The `forerunner` CLI, MCP server, pre-commit hook, CI workflow, and PyPI package are all live. Docker image and Makefile are not present.
10+
This skill does not bundle a runtime. It routes the host agent into the codeforerunner prompt pack (`src/codeforerunner/prompts/` in the source repo, or retrieved via `forerunner generate --prompt-only <task>` from the installed CLI). The `forerunner` CLI, MCP server, pre-commit hook, CI workflow, and PyPI package are all live. Docker image and Makefile are not present.
11+
12+
**Skill mode:** When invoked via a host agent (Claude Code, Codex, Gemini CLI, etc.), the agent itself is the model — no API key or external provider is needed. Use `forerunner generate --prompt-only <task>` to output the assembled prompt bundle, then act on it directly. Reserve `forerunner generate <task>` (without `--prompt-only`) for standalone CLI use with a configured provider.
1113

1214
## When To Activate
1315

@@ -40,7 +42,7 @@ If inputs are missing, the host agent may ask once **before starting** a task to
4042

4143
## Context Layers
4244

43-
These compose once into the prompt sent to the host model. They are not per-task steps. Retrieve each via `forerunner doc <task>` or read directly from `src/codeforerunner/prompts/`.
45+
These compose once into the prompt sent to the host model. They are not per-task steps. Retrieve each via `forerunner generate --prompt-only <task>` or read directly from `src/codeforerunner/prompts/`.
4446

4547
- **System rules.** `system/base.md` — governing role and accuracy contract. Every output must obey it.
4648
- **Context shape.** `partials/context-format.md` — target-repo context shape and file-selection rules.
@@ -51,11 +53,11 @@ These compose once into the prompt sent to the host model. They are not per-task
5153

5254
Per-run, in this order:
5355

54-
1. **Scan first.** Run `forerunner doc scan` (or use the `scan` task prompt directly) and capture its YAML output. All documentation-generation and check workflows depend on the scan result. **Exception:** `init-agent-onboarding` may run without a prior scan because it derives onboarding guidance directly from repo evidence and existing instruction files.
55-
2. **Run the matching task prompt.** See `docs/prompt-guide.md` section 3 for the authoritative task → input → output table. Pick the task that matches the user's request and pass the scan result plus any task-specific inputs.
56+
1. **Scan first.** Run `forerunner generate --prompt-only scan` and capture its YAML output. All documentation-generation and check workflows depend on the scan result. **Exception:** `init-agent-onboarding` may run without a prior scan because it derives onboarding guidance directly from repo evidence and existing instruction files.
57+
2. **Run the matching task prompt.** See `docs/prompt-guide.md` section 3 for the authoritative task → input → output table. Run `forerunner generate --prompt-only <task>` to get the assembled prompt bundle, then execute it. Pass the scan result plus any task-specific inputs.
5658
3. **Honor task output contracts.** When a task prompt specifies `<!-- output: path/to/file.md -->`, write the artifact to that path. When it does not, return Markdown for the user to place. Append a `## Gaps` section whenever evidence was insufficient — never silently invent content.
5759

58-
Call a provider directly with `forerunner generate <task>` (add `--stream` for token-by-token output). Check drift using `forerunner check`. Get a health report via `forerunner doctor`.
60+
In skill mode the host agent handles generation — no API key needed. For standalone CLI use with a configured provider: `forerunner generate <task>` (add `--stream` for streaming). Check drift with `forerunner check`. Health report via `forerunner doctor`.
5961

6062
## Safety And Scope Rules
6163

‎bin/install.js‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,20 +172,20 @@ function vscodeExtPresent(needle) {
172172
path.join(os.homedir(), '.cursor/extensions'),
173173
path.join(os.homedir(), '.windsurf/extensions'),
174174
];
175-
const re = new RegExp(needle, 'i');
175+
const lc = needle.toLowerCase();
176176
for (const r of roots) {
177177
if (!fs.existsSync(r)) continue;
178178
let entries; try { entries = fs.readdirSync(r); } catch (_) { continue; }
179-
if (entries.some(e => re.test(e))) return true;
179+
if (entries.some(e => e.toLowerCase().includes(lc))) return true;
180180
}
181181
return false;
182182
}
183183

184184
function cursorExtPresent(needle) {
185185
const dir = path.join(os.homedir(), '.cursor/extensions');
186186
if (!fs.existsSync(dir)) return false;
187-
const re = new RegExp(needle, 'i');
188-
try { return fs.readdirSync(dir).some(e => re.test(e)); } catch (_) { return false; }
187+
const lc = needle.toLowerCase();
188+
try { return fs.readdirSync(dir).some(e => e.toLowerCase().includes(lc)); } catch (_) { return false; }
189189
}
190190

191191
function walkDir(root, depth) {

‎forerunner.config.yaml‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
# Enables drift rules that are relevant to this project's surface area.
33
# R6/R6b/R6-no-docker/R6b-no-makefile intentionally excluded (no Docker or Makefile here).
44

5-
enabled_rules:
6-
- R1-no-cli
7-
- R2-no-pre-commit
8-
- R3-no-ci
9-
- R4-no-installer
10-
- R5-no-python-package
11-
- R7-no-mcp
12-
- R8-no-marketplace
13-
14-
ignore_paths: []
5+
tasks:
6+
check:
7+
enabled_rules:
8+
- R1-no-cli
9+
- R2-no-pre-commit
10+
- R3-no-ci
11+
- R4-no-installer
12+
- R5-no-python-package
13+
- R7-no-mcp
14+
- R8-no-marketplace
15+
ignore_paths: []

‎install.ps1‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ param([Parameter(ValueFromRemainingArguments=$true)][string[]]$Args)
1212

1313
$ErrorActionPreference = "Stop"
1414

15-
$NpmPkg = "codeforerunner"
15+
# Security: pinned to a specific version so one-liners don't execute unreviewed code.
16+
$NpmPkg = "codeforerunner@0.4.1"
1617
$Repo = "derek-palmer/codeforerunner"
18+
$RepoTag = "v0.4.1"
1719
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
1820
$LocalJs = Join-Path $ScriptDir "bin\install.js"
1921

@@ -29,6 +31,6 @@ if (Test-Path $LocalJs) {
2931
if ($npmUp) {
3032
& npx --yes $NpmPkg -- @Args
3133
} else {
32-
& npx --yes "github:$Repo" -- @Args
34+
& npx --yes "github:${Repo}#${RepoTag}" -- @Args
3335
}
3436
}

‎install.sh‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@
1414

1515
set -euo pipefail
1616

17-
NPM_PKG="codeforerunner"
17+
# Security: pinned to a specific version so curl|bash one-liners don't silently
18+
# execute whatever the npm registry or GitHub currently serves as "latest".
19+
NPM_PKG="codeforerunner@0.4.1"
1820
REPO="derek-palmer/codeforerunner"
21+
REPO_TAG="v0.4.1"
1922

2023
# Locate bin/install.js relative to this script (works even when piped through bash)
2124
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-install.sh}")" 2>/dev/null && pwd || echo "")"
@@ -26,9 +29,9 @@ if [[ -n "$SCRIPT_DIR" && -f "$LOCAL_JS" ]]; then
2629
else
2730
# Primary: npm registry. Fallback: GitHub source (in case npm is down).
2831
# Probe the registry with a HEAD request to avoid running npx twice.
29-
if curl -sf --head "https://registry.npmjs.org/${NPM_PKG}/latest" &>/dev/null; then
32+
if curl -sf --head "https://registry.npmjs.org/codeforerunner/latest" &>/dev/null; then
3033
exec npx --yes "${NPM_PKG}" -- "$@"
3134
else
32-
exec npx --yes "github:${REPO}" -- "$@"
35+
exec npx --yes "github:${REPO}#${REPO_TAG}" -- "$@"
3336
fi
3437
fi

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codeforerunner",
3-
"version": "0.4.1",
3+
"version": "0.4.2",
44
"description": "Model-agnostic repository documentation tooling — installs /forerunner-* slash commands into 30+ agent CLIs",
55
"main": "./bin/install.js",
66
"bin": {

‎plugins/codeforerunner/skills/codeforerunner/SKILL.md‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ description: Routes a coding agent through the tracked codeforerunner prompt pac
77

88
The authoritative canonical source for this skill body is `agent/codeforerunner.skill.md` (see the `SPEC.md` I.agent-skill entry). Distribution copies are not independent sources; maintainers should edit the canonical file, then sync the post-frontmatter body into Codex and Claude copies. Downstream packages must preserve this post-frontmatter Markdown content verbatim (`SPEC.md` V10); per-agent YAML frontmatter may differ, but the body cannot.
99

10-
This skill does not bundle a runtime. It routes the host agent into the codeforerunner prompt pack (`src/codeforerunner/prompts/` in the source repo, or retrieved via `forerunner doc <task>` from the installed CLI). The `forerunner` CLI, MCP server, pre-commit hook, CI workflow, and PyPI package are all live. Docker image and Makefile are not present.
10+
This skill does not bundle a runtime. It routes the host agent into the codeforerunner prompt pack (`src/codeforerunner/prompts/` in the source repo, or retrieved via `forerunner generate --prompt-only <task>` from the installed CLI). The `forerunner` CLI, MCP server, pre-commit hook, CI workflow, and PyPI package are all live. Docker image and Makefile are not present.
11+
12+
**Skill mode:** When invoked via a host agent (Claude Code, Codex, Gemini CLI, etc.), the agent itself is the model — no API key or external provider is needed. Use `forerunner generate --prompt-only <task>` to output the assembled prompt bundle, then act on it directly. Reserve `forerunner generate <task>` (without `--prompt-only`) for standalone CLI use with a configured provider.
1113

1214
## When To Activate
1315

@@ -40,7 +42,7 @@ If inputs are missing, the host agent may ask once **before starting** a task to
4042

4143
## Context Layers
4244

43-
These compose once into the prompt sent to the host model. They are not per-task steps. Retrieve each via `forerunner doc <task>` or read directly from `src/codeforerunner/prompts/`.
45+
These compose once into the prompt sent to the host model. They are not per-task steps. Retrieve each via `forerunner generate --prompt-only <task>` or read directly from `src/codeforerunner/prompts/`.
4446

4547
- **System rules.** `system/base.md` — governing role and accuracy contract. Every output must obey it.
4648
- **Context shape.** `partials/context-format.md` — target-repo context shape and file-selection rules.
@@ -51,11 +53,11 @@ These compose once into the prompt sent to the host model. They are not per-task
5153

5254
Per-run, in this order:
5355

54-
1. **Scan first.** Run `forerunner doc scan` (or use the `scan` task prompt directly) and capture its YAML output. All documentation-generation and check workflows depend on the scan result. **Exception:** `init-agent-onboarding` may run without a prior scan because it derives onboarding guidance directly from repo evidence and existing instruction files.
55-
2. **Run the matching task prompt.** See `docs/prompt-guide.md` section 3 for the authoritative task → input → output table. Pick the task that matches the user's request and pass the scan result plus any task-specific inputs.
56+
1. **Scan first.** Run `forerunner generate --prompt-only scan` and capture its YAML output. All documentation-generation and check workflows depend on the scan result. **Exception:** `init-agent-onboarding` may run without a prior scan because it derives onboarding guidance directly from repo evidence and existing instruction files.
57+
2. **Run the matching task prompt.** See `docs/prompt-guide.md` section 3 for the authoritative task → input → output table. Run `forerunner generate --prompt-only <task>` to get the assembled prompt bundle, then execute it. Pass the scan result plus any task-specific inputs.
5658
3. **Honor task output contracts.** When a task prompt specifies `<!-- output: path/to/file.md -->`, write the artifact to that path. When it does not, return Markdown for the user to place. Append a `## Gaps` section whenever evidence was insufficient — never silently invent content.
5759

58-
Call a provider directly with `forerunner generate <task>` (add `--stream` for token-by-token output). Check drift using `forerunner check`. Get a health report via `forerunner doctor`.
60+
In skill mode the host agent handles generation — no API key needed. For standalone CLI use with a configured provider: `forerunner generate <task>` (add `--stream` for streaming). Check drift with `forerunner check`. Health report via `forerunner doctor`.
5961

6062
## Safety And Scope Rules
6163

‎plugins/codeforerunner/skills/forerunner-api-docs/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ User asks to: document the API, generate API reference, write endpoint docs, cre
2121

2222
## Execute
2323

24-
Run `forerunner doc api-docs` to compose the full prompt with system rules, then execute it.
24+
Run `forerunner generate --prompt-only api-docs` — outputs the assembled prompt bundle to stdout. Read this output and execute the documentation task it describes.
2525

2626
Without CLI, get the prompt from:
2727
- `src/codeforerunner/prompts/tasks/api-docs.md`

0 commit comments

Comments
 (0)