Skip to content

[Bug] init silently destroys ~/.claude/settings.json when existing file fails to parse as JSON #93

@CyanoTex

Description

@CyanoTex

Summary

repowise init rewrites ~/.claude/settings.json with a minimal stub
(mcpServers.repowise + the two PreToolUse/PostToolUse hooks) whenever it
can't parse the existing file as JSON, silently discarding every other key
the user had configured — permissions, enabledPlugins, extraKnownMarketplaces,
other mcpServers, other hooks, and so on.

Because any transient or unrelated JSON issue (stray trailing comma, a comment,
a partial write from another tool, a BOM, an encoding mismatch, etc.) trips
the same path, this is real user data loss triggered by something outside
init's control. On one run I watched ~/.claude/settings.json go from 178
lines to 42 lines: all permissions, 24 enabled plugins, 6 known marketplaces,
and an unrelated MCP server entry were erased.

Reproduction

  1. Have a ~/.claude/settings.json with any meaningful content.

  2. Introduce any parse error — e.g. a trailing comma before a }:

    "hooks": {
      "PreCompact": [ ... ],
    },
  3. Run repowise init <any repo> --index-only -y.

  4. Observe ~/.claude/settings.json: everything except mcpServers.repowise
    and the PreToolUse/PostToolUse hooks is gone. No warning, no backup,
    exit code 0.

Root cause

packages/cli/src/repowise/cli/mcp_config.py — three call sites all share
the same silent-fallback pattern:

  • _merge_mcp_entry (≈line 93):
    try:
        existing = json.loads(config_path.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        existing = {}
  • install_claude_code_hooks (≈line 176): same idiom.
  • save_root_mcp_config (≈line 72): same idiom (less catastrophic
    because it writes a project-local .mcp.json rather than the global
    settings file, but same anti-pattern).

In every case, after swallowing the parse error the code proceeds to
settings_path.write_text(json.dumps(existing, indent=2) + "\n"), which
overwrites the user's file with the empty base plus whatever entry
init wanted to add.

This pattern directly violates the project's own documented blocker rule
"Silent fallbacks — raise or log, don't quietly default."

Recommended fix

Do not treat a parse failure on an existing file as "no prior config."
Two acceptable strategies:

  1. Abort and report: raise / log an explicit error pointing at the
    settings path, tell the user what failed to parse, and exit without
    writing. Let the user resolve the file by hand.
  2. Backup and warn: copy the unparseable file to
    settings.json.repowise-backup-<timestamp> and emit a visible warning
    on stderr before writing the new content.

A missing file is still fine to treat as {} — the hazard is specifically
the FileExists && ParseFails branch.

The same fix should apply to all three call sites, ideally via a shared
helper so the behavior can't drift again.

Adjacent smells (not part of this bug, flagging for context)

  • The PreToolUse/PostToolUse hooks install repowise augment as a bare
    command name. If the user's PATH doesn't resolve repowise to the uv-installed
    binary with the augment subcommand, every Grep/Glob/Bash tool call in
    Claude Code spams a No such command 'augment' error until the hooks
    are manually removed.
  • install_claude_code_hooks writes the repowise MCP server into the
    global settings.json pointing at the target repo — every
    init <dir> invocation rebinds the user's global MCP to the last-indexed
    path. A per-project .mcp.json (already produced by
    save_root_mcp_config) would avoid the pollution.

Both are worth a separate look; happy to file follow-ups if useful.

Severity

High — silent user-config destruction. No warning, no backup, no exit code.
Recoverable only if the user has shell history / a backup / version
control on their dotfiles.


Note from the reporter

This was found while working on
#89 , and TBH, Claude either
made a whoopsie while working on the PR, oooooor the whoopsie was already
there. Either way, having found this while working on it wasn't... fun.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions