Skip to content

fix(configure): use ~/.claude/rules/ for claude-cli, not CLAUDE.md#35

Open
t-dub wants to merge 6 commits into
snyk:mainfrom
t-dub:feat/claude-cli-rules-dir
Open

fix(configure): use ~/.claude/rules/ for claude-cli, not CLAUDE.md#35
t-dub wants to merge 6 commits into
snyk:mainfrom
t-dub:feat/claude-cli-rules-dir

Conversation

@t-dub
Copy link
Copy Markdown

@t-dub t-dub commented May 5, 2026

Summary

When mcp configure --tool=claude-cli runs today, it injects
rules formated block into the user's global ~/.claude/CLAUDE.md.

User-level rules is the correct location for the way this rule is formatted. I agree that this is best suited to a discrete rule as it makes clear what the source is, but injecting it into the user's global CLAUDE.md isn't the right spot.

This PR moves the claude-cli host to the dedicated-file pattern that the
codebase already uses for Cursor (globalSkillsPath), and mirrors the
existing legacyLocalRulesPath migration mechanism with a new
legacyGlobalRulesPath field so existing installs are upgraded cleanly.

Behavior change

Before After
Rules destination ~/.claude/CLAUDE.md (delimited inject) ~/.claude/rules/snyk-security.md (dedicated file)
Uninstall Delimiter-based replace inside CLAUDE.md os.Remove of the dedicated file
Migration on next configure n/a Old delimited block stripped from ~/.claude/CLAUDE.md (idempotent — surrounding user content preserved)

Migration safety

The cleanup is intentionally non-destructive:

  • removeGlobalRules only removes content between
    <!--# BEGIN SNYK GLOBAL RULE --> and <!--# END SNYK GLOBAL RULE -->.
  • If the user has hand-edited their ~/.claude/CLAUDE.md around the Snyk
    block, those edits are preserved.
  • If the user never installed the prior version, the file is untouched.

A new test case (TestRemoveGlobalRulesIsClaudeCliMigrationSafe) verifies
this round-trip on a CLAUDE.md that mixes user content and a delimited
Snyk block.

Test plan

  • go test ./... — all existing tests still pass
  • go vet ./... — clean
  • make lint — 0 issues (GOTOOLCHAIN=go1.25.7 to match the pinned
    golangci-lint v2.6.1 build environment on machines running newer Go)
  • New TestGetHostConfig_ClaudeCliPaths pins the exact paths
  • New TestRemoveGlobalRulesIsClaudeCliMigrationSafe exercises the
    migration cleanup against a representative pre-existing CLAUDE.md
  • TestGetHostConfig updated for claude-cli — now expects
    globalSkillsPath and legacyGlobalRulesPath

🤖 Generated with Claude Code

Claude Code auto-loads any *.md file under ~/.claude/rules/ on every session
(see https://code.claude.com/docs/en/memory#user-level-rules), so the Snyk
rules belong in their own dedicated file rather than injected as a delimited
block into the user's global ~/.claude/CLAUDE.md.

Why this matters:

- ~/.claude/CLAUDE.md is the user's personal preferences file. Tool-managed
  content there is unwelcome pollution and survives uninstall as a leftover
  delimited block unless removeGlobalRules runs successfully.
- A dedicated file uninstalls cleanly with a single os.Remove instead of a
  delimiter-based search-and-replace inside a shared file.
- Mirrors the pattern already used for Cursor (globalSkillsPath →
  ~/.cursor/skills/snyk-rules/SKILL.md) — claude-cli was the inconsistent
  one in the existing codebase.

Migration safety: a new legacyGlobalRulesPath field on hostConfig points at
the prior ~/.claude/CLAUDE.md location. Both addConfiguration and
removeConfiguration now invoke removeGlobalRules against it (idempotent —
no-op when the delimited block is absent), so users upgrading from a prior
install have the old block automatically stripped from their CLAUDE.md
without losing any of their own personal content around it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@t-dub t-dub requested a review from a team as a code owner May 5, 2026 19:55
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 5, 2026

CLA assistant check
All committers have signed the CLA.

@snyk-io
Copy link
Copy Markdown

snyk-io Bot commented May 5, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

t-dub added 5 commits May 5, 2026 15:41
Adds a new globalDedicatedRulesPath field on hostConfig distinct from
globalSkillsPath, and embeds Claude-Code-specific rules content under
internal/configure/rules/claude/ that omits the Cursor SKILL.md
frontmatter (name:/description:) it doesn't recognize.

Before this commit the claude-cli host wrote Cursor-style SKILL
frontmatter into ~/.claude/rules/snyk-security.md. Six tribunal agents
flagged this independently as a content/format mismatch: Claude Code's
user-level-rules schema does not recognize the SKILL keys, and relying
on the loader silently ignoring unknown frontmatter is fragile. The
split also resolves the leaky-abstraction concern that
"globalSkillsPath" was lying for claude-cli — the path pointed at a
rules directory, not a skills one.

Cursor continues to use globalSkillsPath unchanged. claude-cli now uses
globalDedicatedRulesPath, which is wired into the same writeGlobalSkills
machinery (it's just "write a whole file, no delimiters" — the function
name remains accurate to its semantics, even if it predated this split).

Addresses Tribunal finding #F1 (content/format mismatch, score 82) from review
tribunal-2026-05-05T20-11-37Z-pr-35-f6cf7a69.json.
Addresses Tribunal finding #F7 (naming / leaky abstraction, score 75) from review
tribunal-2026-05-05T20-11-37Z-pr-35-f6cf7a69.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

-- ᴛ-ᴅᴜʙ's Review Tribunal ⚖️
…s gate

Previously the migration cleanup that strips the old delimited Snyk block
from ~/.claude/CLAUDE.md sat inside `if configureRules { ... }` in both
addConfiguration and removeConfiguration. A user invoking
`studio-mcp configure --tool claude-cli --no-rules` (MCP only) — a
perfectly reasonable invocation — would never have their old CLAUDE.md
Snyk block cleaned up, even though Snyk has abandoned that location.

Migration housekeeping is conceptually independent of rules configuration:
once we've decided the legacy block is stale (the prior PR did), it should
be cleaned up regardless of whether the user is configuring rules this run.
This commit moves only the legacyGlobalRulesPath cleanup out of the gate;
legacyLocalRulesPath stays inside (its semantics — "you're migrating from
local to global rules" — are inherently tied to rules being configured).

Addresses Tribunal finding #F12 (migration completeness — gating, score 60)
from review tribunal-2026-05-05T20-11-37Z-pr-35-f6cf7a69.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

-- ᴛ-ᴅᴜʙ's Review Tribunal ⚖️
…on test

Adds TestConfigure_ClaudeCliEndToEnd, which drives Configure() — not the
write/remove helpers — for the claude-cli host against a temp HOME and
asserts the full migration story:

  1. add: writes ~/.claude/rules/snyk-security.md with the new
     Claude-Code-shaped content (no Cursor SKILL frontmatter), strips
     the delimited Snyk block from a pre-existing CLAUDE.md, and
     preserves user content above and below the block.
  2. remove: deletes the dedicated rules file and strips the legacy
     block; user content survives.
  3. MCP-only configure (configureRules=false): legacy CLAUDE.md cleanup
     still runs (pinning the F12 behavior contract — migration
     housekeeping is independent of whether rules are configured).

Pre-existing tests pinned the helper-level behavior of removeGlobalRules
but never exercised the wiring through addConfiguration /
removeConfiguration — so a future refactor that dropped the migration
call sites would slip past every other test in this package. This test
catches that.

Implementation notes: a small noopUserInterface stub satisfies
ui.UserInterface without polluting test output. The test is skipped on
Windows because t.Setenv("HOME",...) does not redirect
os.UserHomeDir() there (it reads USERPROFILE) — the migration logic
itself is platform-agnostic; we just rely on Linux/macOS to drive it.

Addresses Tribunal finding #F3 (test gap / integration-untested behavior, score 80) from review
tribunal-2026-05-05T20-11-37Z-pr-35-f6cf7a69.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

-- ᴛ-ᴅᴜʙ's Review Tribunal ⚖️
… content

removeDelimitedBlock and upsertDelimitedBlock previously located markers
via strings.Index (first occurrence). A user's CLAUDE.md that quoted the
literal RuleStart token in a sentence above the real Snyk block — e.g. a
how-to doc explaining the migration — would have everything between the
user's quote and Snyk's real RuleEnd silently deleted by the migration
cleanup. The new migration runs unconditionally on every claude-cli
install, so the failure mode would compound over time.

This commit replaces the index-based search with line-anchored matching:
both markers must appear on their own line (no surrounding text), which is
exactly what writeGlobalRules / writeGlobalSkills emit. User content that
quotes the marker text inline within a sentence no longer matches and is
preserved. When multiple complete delimited pairs exist (e.g. user pasted
the marker on its own line in a fenced code block AFTER the real block),
the new findDelimitedBlockLines helper scans from the end and returns the
last START that has a matching END after it — biased toward the real block
rather than orphan user-quoted markers.

A new TestRemoveDelimitedBlock_LineAnchored_PreservesUserQuotedMarkers
pins both pathological cases. The existing TestRemoveDelimitedBlock and
TestUpsertDelimitedBlock cases pass unchanged — the line-anchored
algorithm produces identical output for any input the prior implementation
got right; it differs only in the user-quoted-marker case it was getting
wrong.

Addresses Tribunal finding #F2 (data destruction / migration safety, score 80) from review
tribunal-2026-05-05T20-11-37Z-pr-35-f6cf7a69.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

-- ᴛ-ᴅᴜʙ's Review Tribunal ⚖️
…leanup in UI

Extracts the four near-identical legacy-cleanup blocks (legacy_local +
legacy_global, in both addConfiguration and removeConfiguration) into two
small helpers: cleanupLegacyLocalRules (workspace-relative, quiet) and
cleanupLegacyGlobalRules (announces in the CLI, surfaces failures with a
concrete file path so the user can inspect manually).

The two-helper split (rather than one bundled cleanupLegacyPaths) reflects
the different gating: cleanupLegacyLocalRules is still called from inside
the configureRules block (its local→global migration semantics are tied
to rules being configured this run), while cleanupLegacyGlobalRules is
called from outside the gate (migration housekeeping is unconditional —
see prior commit ac19d72).

The user-facing breadcrumb on legacy global cleanup matters because the
whole point of moving claude-cli rules out of CLAUDE.md is to stop
silently mutating a user-owned file. Announcing the cleanup before it
runs and surfacing failures keeps that contract honest: a user who's now
told their CLAUDE.md will be edited can see when it actually happens.

Addresses Tribunal finding #F4 (duplication / DRY, score 80) from review
tribunal-2026-05-05T20-11-37Z-pr-35-f6cf7a69.json.
Addresses Tribunal finding #F10 (silent failure / user-visibility, score 65) from review
tribunal-2026-05-05T20-11-37Z-pr-35-f6cf7a69.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

-- ᴛ-ᴅᴜʙ's Review Tribunal ⚖️
@t-dub
Copy link
Copy Markdown
Author

t-dub commented May 5, 2026

I heard from the team that the new direction is actually going to be hooks, via [this](https://github.com/snyk/studio-recipes/tree/main/installer] or something similar, so presumably the logic I'm touching here is going to be retired wholesale. I'll leave the PR open for now, but I'm assuming it's mostly irrelevant with that direction in mind.

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