Skip to content

fix: gate .kimchi/ and .claude/ project resources behind project trust - #1215

Open
mikenorgate wants to merge 7 commits into
masterfrom
fix/project-trust-gate
Open

mikenorgate wants to merge 7 commits into
masterfrom
fix/project-trust-gate

Conversation

@mikenorgate

@mikenorgate mikenorgate commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Linked issue

Closes #

No GitHub issue linked yet — happy to link one if the team creates it.

What does this PR do?

Closes a security hole where a cloned repo's project-local .kimchi/ (and .claude/) configuration took effect at kimchi startup with no trust check:

  • Code execution via MCP servers (.kimchi/mcp.json spawns stdio processes at session start), bash hooks (.kimchi/hooks/bash/), and kimchi-native/claude hooks (hooks.json, .claude/settings.json)
  • Permission-layer disarmament via .kimchi/permissions.json
  • Prompt injection via skills, personas, and agent memory
  • API-key + conversation theft via a project llmEndpoint override (project sets the endpoint, no key → the user's saved global key is sent there)

Two bugs were fixed:

  1. Detection — pi's trust-requiring scan looked under .config/kimchi/harness only, so a repo shipping .kimchi/ was auto-trusted and the "Trust project folder?" prompt never appeared. The pinned pi patch now scans an 18-entry kimchi/claude list in cwd and ancestors (home excluded), and the prompt/untrusted-warning strings speak as Kimchi.
  2. Enforcement — kimchi's own project-scope readers never consulted trust at all. A new fail-closed, cwd-keyed, ancestor-aware gate (src/project-scope-trust.ts) is now consulted by every project-scope reader: config, permissions, hooks (bash/kimchi-native/claude), skills (roots + configured-path cwd expansion), agents settings/personas/memory, tags, ferments, plans, and MCP servers.

Startup ordering: the gate opens pre-main from persisted decisions, syncs pi's in-session decision at session_start (with resource-cache invalidation), and is wired per ACP session. A canonical TRUST_REQUIRING_PROJECT_RESOURCES constant is CI-cross-checked against the patch's embedded list, so gating a new reader without a scan entry fails tests instead of silently auto-trusting.

User-visible change: every project containing .kimchi/ or .claude/ (including Claude Code repos) now prompts once per folder. Opt out with defaultProjectTrust: "always" in global settings. See docs/project-trust.md for the full trust model, the first-trusted-session timing split, and the release note.

Verification

  • Full unit suite: 10,926 tests pass; pnpm run check clean
  • TUI e2e (tests/e2e/tui/project-trust-gate.test.ts, 3/3): an untrusted repo's malicious .kimchi/config.json (attacker endpoint + key) receives zero traffic; a pre-enabled hostile bash hook never executes (marker file absent) and hostile skill text never reaches any chat request body; after a persisted Trust, the project endpoint/key apply on the next launch
  • Per-reader fail-closed unit regressions for nine readers plus the patch-sync cross-check test

Review follow-ups (known, non-blocking)

  • Six newer gates (mcp-adapter, claude-code-hook-adapter, objective-file, agents settings, findAgentFile, readMemoryIndex) still need their per-reader fail-closed unit tests — the docs' "each gated reader" claim will be narrowed or the tests added in a follow-up
  • .kimchi/thinking-steps.json (preference read) needs a gate-or-document decision
  • Memory-dir write side into untrusted projects needs gating (read side is gated)

Checklist

  • I have read CONTRIBUTING.md and agree to the CLA
  • This PR links to an open issue above
  • Tests pass locally (pnpm run test)
  • Lint passes (pnpm run check)
  • Documentation updated if behavior changed (docs/project-trust.md)

@mikenorgate mikenorgate added the bug Something isn't working label Sep 16, 2026
@kimchi-review

kimchi-review Bot commented Sep 16, 2026

Copy link
Copy Markdown

Kimchi Code Review

Property Value
Commit 22cfb92
Author @mikenorgate
Files changed 55
Review status Completed
Comments 6 (4 info, 2 warning)
Duration 85s

Summary

📊 Review Score: 82/100 (overall code quality — 0 lowest, 100 highest)
⏱️ Estimated effort to review: 5/5 (1 = trivial, 5 = very complex)

🧪 Tests: yes — Coverage is strong: every gated reader gains a fail-closed regression test (config.test.ts, tags.test.ts, kimchi-hooks/definition.test.ts, permissions/config.test.ts, claude-code-skills/definition.test.ts, prompt-enrichment.test.ts), the docs promise a unit cross-check between the patch's embedded KIMCHI_TRUST_REQUIRING_PROJECT_RESOURCES copy and TRUST_REQUIRING_PROJECT_RESOURCES, and an E2E test (tests/e2e/tui/project-trust-gate.test.ts) verifies a malicious cloned repo gets zero traffic while untrusted. Two caveats: the core gate module (src/project-scope-trust.ts, src/project-trust.ts) and several gated readers were omitted from the provided diff (truncated), so their tests could not be examined; and several test files (e.g. agents/skill-loader.test.ts, ferment-v2/evaluator.test.ts) call setProjectScopeTrusted without an afterEach reset, relying on vitest module isolation.

📝 Found 6 issue(s). See inline comments for details. (1 finding(s) referenced lines no longer in the diff and were omitted.)

What to expect

Kimchi will analyze the changes in this pull request and post:

  • A summary of the overall changes
  • Inline comments on specific lines with findings categorized by issue type

The review typically completes within a few minutes. This comment will be updated once the review is ready.

Interact with Kimchi
  • @getkimchi review — re-trigger a full review on the latest commit
  • @getkimchi summary — regenerate the PR summary
  • @getkimchi ignore — skip this PR (no review will be posted)
  • Reply to any inline comment to ask follow-up questions or request clarification
Configuration

Reviews are configured by your organization admin.
Review instructions, excluded directories, and severity thresholds can be adjusted per repository in the Kimchi dashboard.


Powered by Kimchi — AI-powered code review by CAST AI

@readme-ai-writer

readme-ai-writer Bot commented Sep 16, 2026

Copy link
Copy Markdown

Documentation Changes Added

Page Section Action Summary
kimchi-cliGuides📝 UpdatedAdd Project Trust section documenting the trust gate for .kimchi/ and .claude/ project resources, and update Per-Project Config, Config Precedence, Bash Hooks, Kimchi Hooks, and Claude Code Hook Adapter sections to mention trust gating.
Add project trust callout to Per-Project Config section and trust note to Config Precedence.
Add project trust gating note to the bash hooks resource discovery paragraph.
Add project trust gating note to the Bash Hooks project location.
Add project trust gating notes to Claude Code Hook Adapter project and local settings paths.
Add project trust gating note to Kimchi Hooks Adapter hook file locations.
Add Project Trust section documenting the trust gate for .kimchi/ and .claude/ project resources.
coding-skillsGuides📝 UpdatedAdd project trust gating notes to the Kimchi project and Claude Code skill directory entries, and add a trust note to the Project skill scoping section.
Add project trust gating notes to Kimchi project and Claude Code skill directory entries, and add trust note to Project skill scoping section.
coding-mcp-serversGuides📝 UpdatedAdd note about project-level .kimchi/mcp.json config and its trust gating.

🔗 View all changes in ReadMe


Actions

  • Merge documentation branch with PR merge
  • Delete documentation branch with PR close

If neither actions are selected, on PR close/merge the docs branch in ReadMe will remain open.

@kimchi-review kimchi-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📊 Review Score: 82/100 (overall code quality — 0 lowest, 100 highest)
⏱️ Estimated effort to review: 5/5 (1 = trivial, 5 = very complex)

🧪 Tests: yes — Coverage is strong: every gated reader gains a fail-closed regression test (config.test.ts, tags.test.ts, kimchi-hooks/definition.test.ts, permissions/config.test.ts, claude-code-skills/definition.test.ts, prompt-enrichment.test.ts), the docs promise a unit cross-check between the patch's embedded KIMCHI_TRUST_REQUIRING_PROJECT_RESOURCES copy and TRUST_REQUIRING_PROJECT_RESOURCES, and an E2E test (tests/e2e/tui/project-trust-gate.test.ts) verifies a malicious cloned repo gets zero traffic while untrusted. Two caveats: the core gate module (src/project-scope-trust.ts, src/project-trust.ts) and several gated readers were omitted from the provided diff (truncated), so their tests could not be examined; and several test files (e.g. agents/skill-loader.test.ts, ferment-v2/evaluator.test.ts) call setProjectScopeTrusted without an afterEach reset, relying on vitest module isolation.

📝 Found 6 issue(s). See inline comments for details. (1 finding(s) referenced lines no longer in the diff and were omitted.)

Comment thread src/extensions/ferment-v2/objective-file.ts
Comment thread src/cli.ts
Comment thread src/cli.ts
Comment thread src/extensions/agents/index.ts
Comment thread src/cli.ts
Comment thread src/extensions/agents/skill-loader.test.ts
mikenorgate added a commit that referenced this pull request Sep 16, 2026
CI root cause: master's newer tests called promptEnrichmentExtension
with the old array signature; the merge exposed them against the
getter-only signature. Fixed the two call sites.

Review fixes (kimchi-review bot, PR #1215):

- objective-file: gate the save side too — while the project is
  untrusted, saveObjectiveFile returns the raw text instead of writing
  .kimchi/plans/*-objective.md (writing into an untrusted repo would
  arm the trust prompt on the next launch via the .kimchi/plans scan
  entry); regression test added
- agents memory: skip ensureMemoryDir for project/local scopes in an
  untrusted project (same self-arming write-side class as above; read
  side was already gated)
- config: permission warnings are now emitted once per path per
  process — the lazy configuredSkillPaths getter re-runs loadConfig on
  every resources_discover, which previously re-printed the chmod-600
  warning per event
- cli: configuredSkillPaths preserves the pre-change dedup
  ([...new Set(...)]) so duplicate config entries don't multiply
  downstream expansion work
- cli: the pre-main trust gate now falls back to the entry.ts default
  agent dir when KIMCHI_CODING_AGENT_DIR is unset, instead of silently
  skipping gate priming
- mcp-adapter: loadMcpConfig and getServerProvenance take a cwd
  parameter (session-scoped callers pass ctx.cwd; ACP sessions with a
  cwd differing from the server process cwd now gate on their own
  tree); project-relative import paths resolve against it too
- agents /agents menu: findAgentFile and projectAgentsDir take the
  session cwd from the command context
- tests: afterEach resetProjectScopeTrustForTests added to the files
  that set trust without a matching reset (skill-loader x2,
  ferment-v2 evaluator/index, permissions index) so the module-global
  gate cannot leak between tests

Co-Authored-By: Kimchi <noreply@kimchi.dev>
mikenorgate and others added 6 commits September 18, 2026 08:12
A cloned repo shipping project-local config took effect at startup without
the trust prompt: pi's trust-requiring scan looked under
.config/kimchi/harness (never .kimchi/), and kimchi's own readers never
consulted the trust decision at all. That gave an untrusted repo code
execution via bash hooks and skills, permission-layer disarmament via
.kimchi/permissions.json, and API-key + conversation theft via a project
llmEndpoint override.

- Patch pi's hasTrustRequiringProjectResources to also scan .kimchi/ and
  .claude/ trust-requiring entries, so the standard "Trust project folder?"
  prompt fires (and headless resolution fail-closes) for repos shipping them
- Add a fail-closed, cwd-keyed project-scope gate (src/project-scope-trust.ts)
  consulted by every project-scope reader: loadConfig, permissions, bash
  hooks, claude-code hooks/skills, skill roots, agent settings/personas/
  memory, tags, and ferment-v2 objective files
- Open the gate from persisted decisions before the pre-main config read
  (resolvePreMainProjectTrust) and sync pi's in-session decision at
  session_start (settings-trust-sync), so trusted projects keep working
- Make config-derived skill paths lazy so post-trust resource discovery sees
  current config instead of a startup snapshot
- Regression tests per reader + TUI e2e: an untrusted repo's malicious
  .kimchi/config.json receives zero traffic; a persisted Trust applies the
  project endpoint and key on the next launch

Co-Authored-By: Kimchi <noreply@kimchi.dev>
Extend the project-trust regression coverage so declining the prompt is
proven inert, not just config-inert:

- unit (bash-hooks): a project hook the user pre-enabled in global settings
  is not executed while untrusted, and runs again once trusted
- unit (prompt-enrichment): resources_discover contributes no .kimchi/
  or .claude/ skill roots while untrusted, and the project root once
  trusted
- e2e: a hostile project surface (executable bash hook with a marker
  file, prompt-injection skills under .kimchi/ and .claude/, hook
  pre-enabled in user settings) — after "Do not trust", the bash tool
  call completes without the hook ever executing (marker absent) and no
  chat request body carries the hostile skill names or descriptions

Also harden the persisted-decision e2e relaunch wait (full process spawn
after quit) with an explicit 30s startup timeout.

Co-Authored-By: Kimchi <noreply@kimchi.dev>
Review of the project-trust gate found readers with program-execution or
prompt-injection semantics that were neither gated nor trust-scanned, plus
detection/enforcement scope mismatches and maintainability debt. This closes
all of them:

- Gate .kimchi/mcp.json (loadMcpConfig + getServerProvenance): an untrusted
  repo must not register (and thereby spawn) MCP servers at session start
- Gate .kimchi/hooks.json and hooks.local.json (kimchi-hooks adapter,
  mirroring the claude-code-hook-adapter) and register discovered kimchi
  hooks in the dynamic resource definitions so their default-enabled
  execution is visible and toggleable in /resources
- Gate .kimchi/ferments (resolveFermentsDir falls back to the user-global
  store while untrusted, so shipped ferment state neither loads nor gets
  mutated by crash recovery)
- Ancestor-walk the kimchi/claude trust-scan entries in the pi patch (with
  the user's home excluded), matching the ancestor-walking skills/tags
  readers — launching from a repo subdirectory no longer skips the prompt
- Gate the cwd expansion of relative configured skill paths
  (expandConfiguredSkillPaths), closing the extraPaths route that the
  default first-run setup's relative .claude/skills/.pi entries fed
- Gate the /agents menu's project file reads (findAgentFile)
- Export TRUST_REQUIRING_PROJECT_RESOURCES as the canonical scan list from
  src/project-scope-trust.ts with a unit test that cross-checks the pi
  patch's embedded copy, so gating a new reader without a scan entry fails
  CI instead of silently auto-trusting
- Make readMemoryIndex's scope/cwd parameters required so the compiler
  enforces its trust gate at every call site
- De-duplicate the config-path classification rule (mapConfigPath now
  returns the cwdResolved classification; isCwdResolvedConfigPath removed)
  and require the getter signature in both skill-path factories, removing
  the union type kept only for array-based test call sites
- Update docs/project-trust.md's trigger table and gated-reader list

Each new gate lands with a fail-closed regression test; the patch entry
list, canonical constant, and docs are kept in sync by construction.

Co-Authored-By: Kimchi <noreply@kimchi.dev>
CI root cause: master's newer tests called promptEnrichmentExtension
with the old array signature; the merge exposed them against the
getter-only signature. Fixed the two call sites.

Review fixes (kimchi-review bot, PR #1215):

- objective-file: gate the save side too — while the project is
  untrusted, saveObjectiveFile returns the raw text instead of writing
  .kimchi/plans/*-objective.md (writing into an untrusted repo would
  arm the trust prompt on the next launch via the .kimchi/plans scan
  entry); regression test added
- agents memory: skip ensureMemoryDir for project/local scopes in an
  untrusted project (same self-arming write-side class as above; read
  side was already gated)
- config: permission warnings are now emitted once per path per
  process — the lazy configuredSkillPaths getter re-runs loadConfig on
  every resources_discover, which previously re-printed the chmod-600
  warning per event
- cli: configuredSkillPaths preserves the pre-change dedup
  ([...new Set(...)]) so duplicate config entries don't multiply
  downstream expansion work
- cli: the pre-main trust gate now falls back to the entry.ts default
  agent dir when KIMCHI_CODING_AGENT_DIR is unset, instead of silently
  skipping gate priming
- mcp-adapter: loadMcpConfig and getServerProvenance take a cwd
  parameter (session-scoped callers pass ctx.cwd; ACP sessions with a
  cwd differing from the server process cwd now gate on their own
  tree); project-relative import paths resolve against it too
- agents /agents menu: findAgentFile and projectAgentsDir take the
  session cwd from the command context
- tests: afterEach resetProjectScopeTrustForTests added to the files
  that set trust without a matching reset (skill-loader x2,
  ferment-v2 evaluator/index, permissions index) so the module-global
  gate cannot leak between tests

Co-Authored-By: Kimchi <noreply@kimchi.dev>
Two CI failure classes, both from the trust gate interacting with the
CI environment:

1. The ancestor walk scanned the OS temp directory and above: CI
   runners carry stray .kimchi//.claude artifacts in /tmp (and dev
   machines can too), which made every temp-dir session
   trust-requiring — breaking the "trivially trusted" unit tests and
   hanging every e2e session at the trust prompt. The walk now stops
   at the temp root (mirroring the home exclusion): the temp dir is
   not a project tree, and a stray entry there must not gate
   unrelated temp sessions. Verified by planting a stray
   .claude/settings.json in tmpdir locally — the previously failing
   tests now pass.

2. E2E fixtures that seed .kimchi/ resources in their workDir (ACP
   plan-updates ferments, TUI ferment persistence tests) had no trust
   decision: ACP is headless and fail-closes, TUI sessions hung at the
   prompt. Both fixtures now pre-record a persisted trust decision for
   the workDir (realpath-keyed, matching pi's canonicalization),
   mirroring what an interactive session's first run persists. The
   TUI fixture gains a trustWorkDir option (default true); the
   project-trust-gate scenarios opt out to keep exercising the live
   prompt.

Verified: full unit suite (11,000), check clean, trust-gate e2e 3/3,
ferment-draft-proposal-persistence, ACP plan-updates 4/4, mcp-stdio
8/8.

Co-Authored-By: Kimchi <noreply@kimchi.dev>
Rebase onto master brought pi 0.85.1 and the published pi-mcp-adapter
refactor; this commit restores the branch's trust functionality on top:

- Re-apply the trust-scan patch hunks to the 0.85.1 package
  (patches/@earendil-works__pi-coding-agent@0.85.1.patch): the
  KIMCHI_TRUST_REQUIRING_PROJECT_RESOURCES entry list checked in cwd and
  ancestors, stopping at the user's home and the OS temp directory, plus
  the Kimchi-branded trust prompt and untrusted-warning strings. Header
  restored with the security section (ticket reference dropped per the
  earlier request); lockfile patch hash re-synced.
- The vendored mcp-adapter is gone upstream; master's new
  src/extensions/mcp facade already gates project MCP config on pi's
  project trust (resolveMcpProjectTrust consults
  hasTrustRequiringProjectResources — which the patch extends with
  .kimchi/mcp.json), so the MCP gate carries over through that chain.
  The accidentally resurrected vendored adapter directory is removed.
- Point the patch cross-check test and doc references at the 0.85.1
  patch file.
- Resolve the cli.ts rebase conflict keeping master's
  mcpAdapterExtensions block and the branch's deduplicated lazy
  configuredSkillPaths getter.

Verified: full unit suite (10,878), tsc + biome clean, trust-gate e2e
3/3, ferment persistence e2e, ACP plan-updates 4/4, scan harness
(mcp.json / ancestor skills trigger, empty tree does not).

Co-Authored-By: Kimchi <noreply@kimchi.dev>
@mikenorgate
mikenorgate force-pushed the fix/project-trust-gate branch from 3a8f71c to 92b3aa9 Compare September 18, 2026 07:23
Diagnosis of the acp/mcp-e2e CI failures after the rebase: the ACP test
"does not execute repository MCP configuration in a headless session
without trust" failed because FermentStorage's constructor eagerly
mkdir-ed <workDir>/.kimchi/ferments on every construction — for a
trivially-trusted, no-resources project the kimchi gate is open, so the
ferment store wrote into it, and that very directory is a
trust-requiring scan entry. The mcp facade's later in-session
resolveMcpProjectTrust then saw hasTrustRequiringProjectResources as
true and short-circuited to trusted, executing the untrusted server.

Fix: FermentStorage no longer creates its directory in the constructor
and list() treats a missing directory as an empty store; writes create
the directory on demand (the save path already mkdirs). A project that
never had ferments no longer sprouts .kimchi/ferments just because
kimchi started, so trust resolutions that run later in the same session
are no longer armed by kimchi's own writes.

Also: TUI fixture pretrust is now conditional on the seeded workDir
actually being trust-requiring (so master's .mcp.json-only untrusted-MCP
scenarios keep working), and the ACP fixture's pretrust is opt-in
(plan-updates, the only test that seeds .kimchi/ into its workDir,
passes pretrustWorkDir: true).

Verified: full unit suite (10,878), check clean, ACP mcp-workflow +
plan-updates 11/11, trust-gate e2e 3/3, mcp-project-trust e2e 2/2,
ferment persistence e2e.

Co-Authored-By: Kimchi <noreply@kimchi.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant