Skip to content

fix(core): resolve config outside the working directory - #59

Open
ffflorian wants to merge 2 commits into
mainfrom
fix/process-dir
Open

fix(core): resolve config outside the working directory#59
ffflorian wants to merge 2 commits into
mainfrom
fix/process-dir

Conversation

@ffflorian

Copy link
Copy Markdown
Collaborator

This fixes #17.

findConfigPath only walked up from process.cwd(). GUI clients spawn the server with a working directory unrelated to the user's project (Claude Desktop reports /Applications, VS Code its own app bundle), so no project or user config was ever found and every tool call reported "No configuration file found".

Resolution order is now the one described in the issue:

  1. KNOWLEDGE_SUBDIR — the directory holding config.yaml. If it has none,
    nothing is loaded; the override is never silently ignored.
  2. Upward search from PROJECT_DIR, when set.
  3. Upward search from the working directory (unchanged behaviour).
  4. ~/.knowledge/config.yaml as a shared fallback.

Notes for review

  • Discovery stays in packages/core/src/config/discovery.ts instead of copying directory-discovery.ts from prompts-mcp: this repo needs a config file path with both an async and a sync variant, and the CLI shares the same code path. A second utility in packages/mcp-server would leave two mechanisms with the CLI still on the old one. Env contract and search order are identical.
  • Discovery takes { includeHome?: boolean } (default true) and create passes false. Without it, create in a project that has no config appends the docset to the user's global ~/.knowledge/config.yaml — this happened during development.
  • Existing tests now pin HOME/USERPROFILE to a temp dir, otherwise a real ~/.knowledge/config.yaml on the developer machine leaks into results.

@ffflorian
ffflorian requested a review from mrsimpson August 20, 2026 11:43
@ffflorian
ffflorian marked this pull request as ready for review August 20, 2026 11:43

@mrsimpson mrsimpson left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed #59. Implementation is clean — no monkeypatches, no any, and the test changes are proper env isolation (save/restore HOME/USERPROFILE in beforeEach/afterEach). The approach is a reasonable shipped-now fix, but it's an env-var + home-fallback workaround for a problem MCP roots was designed to solve (see the note on server.ts).

Two things I'd want resolved before merge:

  1. Write-safety inconsistencycreate gets { includeHome: false }, but init writes without it (inline on create.ts). Either guard init/refresh too or document the intent.
  2. Silent home fallback — a behavior change with no opt-out for strict project-only resolution (inline on discovery.ts).

Minor (non-blocking):

  • candidateConfigPaths() materializes the full ancestor list before probing; an early-return during the walk would avoid the allocation (trivial here, just noting).
  • KNOWLEDGE_SUBDIR reads ambiguously ("subdir of .knowledge" vs "the dir holding config.yaml"); it's documented so this is fine, just slightly confusing.

Docs in README/USER_GUIDE are good — they explain the resolution order and don't hide the env-var friction.

const configExists = await configManager.configExists(cwd);
// The home config is never a write target for a project that has none:
// creating a docset here would silently edit the user's global config
const discovery = { includeHome: false };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This guard is exactly right for create. But init writes to the config too — ensureKnowledgeGitignoreSync(configPath), coreInitDocset(..., configPath, ...) (drops metadata into the .knowledge dir), and updateDocsetPaths(...) (rewrites config.yaml) — all reached via configManager.loadConfig(cwd) in init.ts with no includeHome option. So a project with no local config but a matching docset in ~/.knowledge/config.yaml will have init write into the home config — the same class of bug this fixes here. Suggest extending the guard to init (and reconsidering refresh, which also calls ensureKnowledgeGitignoreSync), or documenting why init-to-home is intended.

}

if (includeHome) {
const homeConfigPath = configPathFor(homedir());

@mrsimpson mrsimpson Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stronger take: I think home support should be opt-in, not the default. The norm is that config lives with the project so it stays up-to-date and versioned; a silent ~/.knowledge/config.yaml fallback masks a missing project config (you never notice you forgot one) and quietly changes behavior for every findConfigPath caller.

Suggest defaulting includeHome to false, and enabling it explicitly where the GUI-launch problem actually lives — i.e. the MCP server path. Two implications to wire:

  • create.ts's explicit { includeHome: false } would then just be the default (can keep for clarity).
  • The server must opt in — either pass { includeHome: true }, or gate it behind an explicit env knob like KNOWLEDGE_HOME_FALLBACK=1. If neither is done, the very bug this PR fixes regresses for GUI clients that don't set PROJECT_DIR/KNOWLEDGE_SUBDIR.

Going fully env-driven keeps it user-controlled and makes the "global docsets" case a deliberate choice rather than an implicit side effect.

"See the search_docs tool description for example configuration.",
"Create .knowledge/config.yaml in your project root or home directory.\n" +
"See the search_docs tool description for example configuration.\n\n" +
"**Option 3: Point at an existing configuration**\n" +

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design question: why not use MCP roots (ListRootsRequestSchema / setRootsListChangedRequestHandler) as the primary mechanism? The SDK supports it and it's the protocol-native channel for "what is my project root" — VS Code would report the workspace folder directly. I can see the rationale for env vars (works regardless of client capability, e.g. Claude Desktop's weak/absent roots support), but it cements a per-client env-var contract that's harder to unwind later. Worth a sentence in the PR/issue on why roots isn't the primary path here.

ffflorian added a commit that referenced this pull request Aug 21, 2026
Review follow-up on #59.

- includeHome now defaults to false. Discovery falls back to
  ~/.knowledge/config.yaml only where a machine-wide config is a legitimate
  answer: the MCP server (its working directory is dictated by the GUI client
  that launched it) and the CLI commands that operate on an already declared
  docset (status, init, refresh). Anything that may create a config, i.e.
  create, keeps the project-only default.
- init/refresh keep the fallback on purpose: docsets declared in
  ~/.knowledge/config.yaml must be manageable from any directory. Their writes
  target the config that declared the docset, which is now documented.
- The init_docset MCP tool no longer passes process.cwd() explicitly, which
  defeated PROJECT_DIR; it resolves the config like the read path does.
- updateDocsetPaths takes the config path the caller resolved instead of
  re-running discovery, and the ConfigManager cache is keyed by start
  directory and options. A clone longer than the 60s cache TTL could
  otherwise make init write discovered paths to a different config.
- candidateConfigPaths is a generator, so probing stops at the first hit
  without materialising the ancestor list.
Review follow-up on #59.

- includeHome now defaults to false. Discovery falls back to
  ~/.knowledge/config.yaml only where a machine-wide config is a legitimate
  answer: the MCP server (its working directory is dictated by the GUI client
  that launched it) and the CLI commands that operate on an already declared
  docset (status, init, refresh). Anything that may create a config, i.e.
  create, keeps the project-only default.
- init/refresh keep the fallback on purpose: docsets declared in
  ~/.knowledge/config.yaml must be manageable from any directory. Their writes
  target the config that declared the docset, which is now documented.
- The init_docset MCP tool no longer passes process.cwd() explicitly, which
  defeated PROJECT_DIR; it resolves the config like the read path does.
- updateDocsetPaths takes the config path the caller resolved instead of
  re-running discovery, and the ConfigManager cache is keyed by start
  directory and options. A clone longer than the 60s cache TTL could
  otherwise make init write discovered paths to a different config.
- candidateConfigPaths is a generator, so probing stops at the first hit
  without materialising the ancestor list.
@ffflorian

ffflorian commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

@mrsimpson

Addressed in 40f31be.

Done

  • includeHome defaults to false now. Opted in at the read paths where a machine-wide config is a legitimate answer: server discovery, the init_docset tool, CLI status/init/refresh. create keeps the project-only default. Skipped a separate KNOWLEDGE_HOME_FALLBACK knob — with the opt-in at the call sites it would express the same thing twice.
  • candidateConfigPaths() is a generator, so probing stops at the first hit.
  • Two related bugs surfaced while wiring this: init_docset passed process.cwd() explicitly, which defeated PROJECT_DIR; and updateDocsetPaths() re-ran discovery through the 60 s ConfigManager cache, so a clone longer than the TTL could write discovered paths to a different config than init read. It now takes the caller-resolved configPath, and the cache is keyed by start dir + options.

Not done: guarding init/refresh
That would break the case the fallback exists for. Both act on an already declared docset, and docsets meant to be available everywhere live in ~/.knowledge/config.yaml; with the guard, init <docset> fails from any directory outside $HOME and there is no other config it could sensibly write to. The line that matters is create-vs-mutate, not read-vs-write: create can invent a config file, so a fallback silently redirects it, while init writes back to the file that declared the docset. Intent is documented in init.ts, refresh.ts and the USER_GUIDE.

KNOWLEDGE_SUBDIR: kept — it is the contract from #17 (an earlier iteration used KNOWLEDGE_CONFIG_PATH and was renamed to match). Docs now spell out that it is the directory holding config.yaml, usually .knowledge itself.

MCP roots: agreed it is the protocol-native channel, just not the primary path here. Discovery is shared with the CLI and has a sync variant, while roots is async, only available after initialize, and capability-dependent — so the env contract stays needed as a fallback either way. Happy to add it as a preferred source above PROJECT_DIR in a follow-up rather than grow this PR.

Build, typecheck, 227 tests, oxlint and prettier clean.

@ffflorian
ffflorian requested a review from mrsimpson August 21, 2026 08:22
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.

Fix process.cwd() issue for GUI-launched apps (Claude Desktop, VS Code)

2 participants