Skip to content

fix(state): refuse mkdir in uninitialized projects#77

Merged
v0lkan merged 1 commit intomainfrom
fix/state-dir-no-mkdir-uninit
May 9, 2026
Merged

fix(state): refuse mkdir in uninitialized projects#77
v0lkan merged 1 commit intomainfrom
fix/state-dir-no-mkdir-uninit

Conversation

@josealekhine
Copy link
Copy Markdown
Member

Closes a cross-IDE leak where opening a non-ctx workspace in Cursor and submitting a single prompt deposited a stub .context/state/ (mode 0750) into the project. Cursor imports Claude Code hooks and sets CLAUDE_PROJECT_DIR to the workspace root for compatibility; the ctx@activememory-ctx plugin's UserPromptSubmit chain then fired in every workspace. The check-reminder hook's "provenance-first" ordering called Preamble -> nudge.Paused -> PauseMarkerPath -> state.Dir before the Initialized() gate, leaving the mkdir as the authoritative source of the leak.

Move the gate inside state.Dir() itself: when the project is not initialized, return errCtx.ErrNotInitialized without mkdir. Hook callers' existing dirErr != nil branches absorb the new sentinel silently; interactive callers (ctx add, ctx task complete, ctx prune) surface a path-bearing message via cobra's standard error path. Refactor cooldown.TombstonePath to delegate to state.Dir so the same gate applies to the PreToolUse "ctx agent" hook path.

Tests:

  • internal/cli/system/core/state: full Dir/Initialized matrix including override bypass and partial-init cases.
  • internal/cli/system/cmd/check_reminder: end-to-end regression that simulates the Cursor flow and asserts no .context/ exists after running the hook in an uninitialized tempdir.
  • pause/resume tests updated to seed the now-required init files (FilesRequired) so state.Dir keeps succeeding.

Spec: specs/state-dir-no-mkdir-when-uninitialized.md

Closes a cross-IDE leak where opening a non-ctx workspace in
Cursor and submitting a single prompt deposited a stub
.context/state/ (mode 0750) into the project. Cursor imports
Claude Code hooks and sets CLAUDE_PROJECT_DIR to the workspace
root for compatibility; the ctx@activememory-ctx plugin's
UserPromptSubmit chain then fired in every workspace. The
check-reminder hook's "provenance-first" ordering called
Preamble -> nudge.Paused -> PauseMarkerPath -> state.Dir
before the Initialized() gate, leaving the mkdir as the
authoritative source of the leak.

Move the gate inside state.Dir() itself: when the project is
not initialized, return errCtx.ErrNotInitialized without
mkdir. Hook callers' existing dirErr != nil branches absorb
the new sentinel silently; interactive callers (ctx add,
ctx task complete, ctx prune) surface a path-bearing message
via cobra's standard error path. Refactor cooldown.TombstonePath
to delegate to state.Dir so the same gate applies to the
PreToolUse "ctx agent" hook path.

Tests:
- internal/cli/system/core/state: full Dir/Initialized matrix
  including override bypass and partial-init cases.
- internal/cli/system/cmd/check_reminder: end-to-end regression
  that simulates the Cursor flow and asserts no .context/ exists
  after running the hook in an uninitialized tempdir.
- pause/resume tests updated to seed the now-required init
  files (FilesRequired) so state.Dir keeps succeeding.

Spec: specs/state-dir-no-mkdir-when-uninitialized.md
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
@v0lkan v0lkan merged commit f958216 into main May 9, 2026
12 checks passed
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.

2 participants