docs: clarify feature directories may live outside the project root - #4739
chelsealong wants to merge 2 commits into
Conversation
SPECIFY_FEATURE_DIRECTORY previously described the selected feature directory as being "within" the resolved project, which implies a containment rule that neither the Bash nor Python core scripts enforce. An absolute SPECIFY_FEATURE_DIRECTORY is used as-is (see scripts/bash/common.sh get_feature_paths), so it may legitimately point outside the project root; only relative paths resolve under it. Clarify that the project root identifies the Spec Kit project and its command/configuration context, while the default feature location (specs/) is just that — a default, not a boundary. Fixes github#4738 Assisted-by: Claude Code (model: Claude Sonnet 5, autonomous)
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Both pages must explicitly document that relative paths may also resolve outside the project root.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 2
Open (2)
What changed in this PR
Clarifies that feature directories are not confined to the Spec Kit project root.
Changes:
- Documents external absolute feature paths.
- Separates project context from feature-document location.
- Still omits explicit external relative paths such as
../shared-feature.
| File | Description |
|---|---|
docs/reference/core.md |
Updates feature-directory resolution documentation. |
docs/guides/monorepo.md |
Clarifies project and feature selection in monorepos. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| the **feature**. A relative feature path resolves under that project root, but | ||
| an explicitly selected feature directory (e.g. an absolute path) is not | ||
| required to live inside it — the project root identifies the Spec Kit project |
| | `SPECKIT_INTEGRATION_DEFAULT` | Override the fallback integration used by `specify init` when `--integration` is omitted (interactive prompt default and non-interactive fallback). Set it to any registered integration key (e.g. `gemini`, `claude`). An unrecognized value is ignored with a warning and the built-in default (`copilot`) is used. An explicit `--integration <key>` always takes precedence. | | ||
| | `SPECIFY_INIT_DIR` | Target a member project from outside its directory (e.g. a monorepo root) without `cd`, for non-interactive / CI use. Set it to the **project root** — the directory *containing* `.specify/` (relative paths resolve against the current directory). The path must exist and contain `.specify/`, otherwise the command errors and does **not** fall back to the current directory. Resolved once in the core root helper (`get_repo_root` in Bash, `Get-RepoRoot` in PowerShell), so it is honored by the core feature scripts (`/speckit.plan`, `/speckit.tasks`, …) and the Git extension's feature-branch creation, which inherit it. The `specify` CLI applies the **same** validation rules to every project-scoped subcommand (`specify integration …`, `specify extension …`, `specify workflow …`, `specify preset …`, and the rest that operate on a `.specify/` project), so those can target a member project too. When unset, Bash/PowerShell helpers keep their existing upward search; the `specify` CLI keeps its project-scoped resolver cwd-only unless a command explicitly defines broader detection (for example, bundle commands). | | ||
| | `SPECIFY_FEATURE_DIRECTORY` | Override the active feature directory *within* the resolved project (takes precedence over `.specify/feature.json`). Relative paths resolve under the project root. Combine with `SPECIFY_INIT_DIR` to pick both the project and the feature non-interactively. | | ||
| | `SPECIFY_FEATURE_DIRECTORY` | Override the active feature directory (takes precedence over `.specify/feature.json`). A relative path resolves under the project root; an absolute path is used as-is and may point outside the project root — the project root still identifies the Spec Kit project and its command/configuration context, but the selected feature directory itself is not required to live under it. Combine with `SPECIFY_INIT_DIR` to pick both the project and the feature non-interactively. | |
…ject root Copilot review on github#4739 noted the previous wording only called out absolute paths as able to escape the project root, while a relative path like ../shared-feature is joined without a containment check and resolves outside it too. Assisted-by: Claude Code (model: Claude Sonnet 5, autonomous)
|
Posted on behalf of @chelsealong by Claude Code (model: Claude Sonnet 5, autonomous); comment fully AI-drafted. Addressed both inline comments in ae3dda9: |

Summary
Fixes #4738.
docs/reference/core.mddescribedSPECIFY_FEATURE_DIRECTORYas selecting a feature directory "within" the resolved project, anddocs/guides/monorepo.mdsaid the same ("the feature within it"). This implies a containment rule that is not intended or enforced.Verified against the actual resolver (
scripts/bash/common.sh,get_feature_paths, lines ~191-215):An absolute
SPECIFY_FEATURE_DIRECTORYis used as-is — it can point anywhere on disk, including outside the project root. Only a relative path is resolved under the project root.scripts/python/common.pydoes the same. So "within the resolved project" was inaccurate: feature directories hold documentation artifacts (spec.md,plan.md, …) and may legitimately live outside the project root, while the project root itself still identifies the Spec Kit project and its command/configuration context.Changes
docs/reference/core.md: reworded theSPECIFY_FEATURE_DIRECTORYtable row and the "Two resolution axes" callout to state that a relative path resolves under the project root, an absolute path is used as-is and may point outside it, and the default feature location (specs/) is a default, not a containment boundary.docs/guides/monorepo.md: reworded the equivalent "selects the feature within it" sentence to the same effect, keeping the existing pointer to the full reference doc.This is documentation-only, as requested by the issue: no runtime behavior, containment enforcement, or script logic was changed.
Test plan
Docs-only change; no unit tests apply to these files (
grep -rl "core.md\|monorepo.md" tests/returns nothing).Ran the repo's markdown lint locally to confirm no new issues were introduced:
Confirmed these two
MD028findings are pre-existing onmain(same output withgit stashapplied, unrelated blockquote formatting a few lines below my edit) — not introduced by this change.AI disclosure
This PR was authored by an autonomous AI agent (Claude Code, model Claude Sonnet 5, running non-interactively/autonomously) on behalf of the repository owner. The agent read the issue, verified the containment claim against the current
scripts/bash/common.sh/scripts/python/common.pyresolver logic, and made the minimal documentation wording change described above.🤖 Generated with Claude Code