Skip to content

fix(init): create Claude config dir for global init - #2521

Merged
KuSh merged 4 commits into
rtk-ai:developfrom
elite133713:fix/global-init-create-claude-config-dir
Sep 25, 2026
Merged

KuSh merged 4 commits into
rtk-ai:developfrom
elite133713:fix/global-init-create-claude-config-dir

Conversation

@elite133713

Copy link
Copy Markdown
Contributor

Summary

  • create the Claude config parent directory before global init writes RTK.md or settings.json
  • fix fresh-home failures where atomic writes could not create temp files inside a missing ~/.claude directory
  • add regression coverage for default global init, hook-only global init, and dry-run behavior with a missing Claude config dir

Fixes #2519.

Context

Plain global init can currently fail on a fresh machine or user profile when ~/.claude does not exist:

rtk: Failed to write RTK.md: ~/.claude/RTK.md: Failed to create temp file in ~/.claude: No such file or directory (os error 2)

The underlying issue is that atomic_write() creates its tempfile inside the target file's parent directory. That parent directory must exist before NamedTempFile::new_in(parent) is called.

Related: #1840 fixes the separate Cursor-only path for rtk init -g --agent cursor and missing ~/.cursor. This PR intentionally stays focused on the default/global Claude init path.

CONTRIBUTING.md alignment

  • Focused PR: one bug fix in the hook init path; no unrelated refactors or formatting churn.
  • Conventional commit: fix(init): create Claude config dir for global init.
  • Target branch: develop.
  • Documentation: not updated because this is a bug fix for existing documented init behavior, not a new hook feature or behavior surface.
  • Changelog: not edited manually; release-please owns it.

Test plan

  • Unit tests added/updated for changed code
  • Edge cases covered: missing Claude config dir, hook-only global init, dry-run with missing dir
  • cargo fmt --all --check
  • cargo test missing_claude_dir -- --nocapture
  • cargo test --all (2212 passed, 8 ignored)
  • cargo clippy --all-targets -- -D warnings
  • Manual test with temp HOME and CLAUDE_CONFIG_DIR: ./target/debug/rtk init -g --auto-patch
  • Snapshot tests not applicable: no snapshots changed
  • Token savings/truncation checklist not applicable: this PR does not add or change output filters

@CLAassistant

CLAassistant commented Jun 19, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@rtk-release-bot

Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as stale due to 90 days of inactivity. If this pull request is still relevant, please leave any comment (for example, "bump"), and we'll keep it open. Your contribution is very much appreciated — we're sorry we haven't been able to review it yet.

Co-authored-by: Cursor <cursoragent@cursor.com>
@elite133713

Copy link
Copy Markdown
Contributor Author

@rtk-release-bot this is still relevant and ready.

Merged current develop and resolved the src/hooks/init.rs test conflict (kept the missing ~/.claude coverage and the newer cwd-guard / UTF-8 BOM tests). Local checks: cargo test missing_claude (3 passed), cargo test utf8_bom (8 passed), cargo test cwd_guard (1 passed). Ready for review and merge.

Co-authored-by: Cursor <cursoragent@cursor.com>
develop split src/hooks/init.rs into src/hooks/init/. The change now sits in
init/mod.rs (ensure_parent_dir, called from write_if_changed) and
init/claude.rs (before the settings.json update_json_file, outside dry-run),
with the regression tests in init/claude.rs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@KuSh KuSh 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.

Round 1 of 3 — APPROVED, follow-ups filed

Claim: rtk init -g (default, --hook-only, --no-patch) creates the missing Claude config directory before its atomic writes instead of failing on a fresh home, while --dry-run still creates nothing.
Scope: accept (round 1, frozen)
Ran: rtk init × 3,064 forms (13 agents + --codex/--copilot/--opencode/--gemini/--claude-md/--hook-only, local/-g, install/uninstall, policy, dry-run, -v, run twice + uninstall) × 6 seeds (empty home, pre-existing user config, custom env dirs, empty env vars, empty/BOM/invalid/null JSON, pre-existing .bak) — develop c50ff30d (after the #3878 split) vs fixup fe3ba462; plus nested and relative CLAUDE_CONFIG_DIR, ~/.claude as a file, read-only HOME. Invariants: non-default targets (CLAUDE_CONFIG_DIR), every decision (create / update / dry-run / uninstall), under the hook (rtk rewrite passes rtk init through unchanged). Savings: n/a, no output filter touched.
Matrix: never — ran this round
CI @fe3ba462: all green (11/11, including test on ubuntu, macos and windows)

Blocks merge (0, frozen at round 1)

None.

The matrix shows 158 diffs, all where they should be. Every exit-code diff goes from 1 to 0: init -g, init -g --auto-patch, init -g --no-patch, init -g --hook-only --auto-patch (± -v) on an empty home, and init -g with CLAUDE_CONFIG_DIR="". There are no diffs at all when ~/.claude already exists, and no crashes.

Optional — will not hold merge

  • with_missing_claude_dir_override sets HOME for the whole test process. It is the only in-process HOME mutation in the lib tests, and temp_env serialises only its own callers. A parallel test reading dirs::home_dir() twice (for example tee_file::test_display_shell_path_uses_home_var_for_home_paths_with_spaces) could see it change in between. I could not trigger this in 800 runs, so it is a note, not a finding. Only the OpenCode test needs HOME; the other three could drop it.
  • --dry-run does not mention that the real run will create the config directory. ensure_pi_extensions_dir prints [dry-run] would create … for the same situation.

Filed as follow-ups

  • #4247 CLAUDE_CONFIG_DIR is used as given when relative or holding a literal ~ — the resolver is outside this PR's hunks. Before this PR, CLAUDE_CONFIG_DIR='~/.claude' rtk init -g failed (exit 1). Now it creates ./~/.claude/ in the current directory and exits 0. Develop already writes to a relative CLAUDE_CONFIG_DIR whenever it exists, so the fix belongs in resolve_claude_dir_from, not here.

  • --agent cursor -g on a fresh home now gets past ~/.claude and fails on the missing ~/.cursor (exit 1 before and after). That is #2097, with #3000 / #3290 open against it.

  • Creating the parent in one shared place (update_json_file / atomic_write) instead of per agent is planned as a separate follow-up PR once this lands, so this PR stays scoped to Claude.

Checked and correct — no need to re-verify

  • Both new call sites are after every dry-run return. The update and allow_read_error branches require path.exists(), so they cannot run with a missing parent.
  • Every other write_if_changed caller (Hermes, Codex, OpenCode, Gemini, Vibe, Copilot) already creates its directory, so for them the new create_dir_all is a no-op.
  • Tests: removing either ensure_parent_dir call, or dropping the !dry_run gate on the settings.json one, each makes one of the new tests fail.
  • ~/.claude as a file and a read-only HOME give Failed to create directory: … (exit 1) instead of the temp-file error.
  • Uninstall leaves settings.json and .bak in ~/.claude, the same as develop when the directory already existed.

Hypotheses built and dropped

  • A dangling ~/.claude/RTK.md symlink gets replaced by a regular file: the parent exists, so the added create_dir_all is a no-op and the path is byte-for-byte develop's.
  • The new tests write the real config on Windows or with XDG_CONFIG_HOME: develop already has seven ungated run_default_mode/run_hook_only_mode tests with the same exposure, and this PR makes it no worse.
  • Test unwrap(): it matches the surrounding test module.

Your questions

None asked.

Next

Fixup fe3ba46 pushed. It merges develop, where #3878 split src/hooks/init.rs into src/hooks/init/, and carries the change over unchanged in substance:

  • ensure_parent_dir is in init/mod.rs, called from write_if_changed.
  • The settings.json call is in init/claude.rs, before update_json_file. It is gated on !dry_run there, because update_json_file now handles dry-run itself.
  • The four tests are in init/claude.rs. The OpenCode test imports its path constants locally, so the cfg(unix) gate leaves nothing unused on Windows.

fmt, clippy and all 3,959 tests pass; a Windows cargo check --all-targets is clean. Approving once CI is green on it.
Rounds: 1/3. Threads: 0 open, 0 resolved this round.

@KuSh
KuSh merged commit 91872ab into rtk-ai:develop Sep 25, 2026
11 checks passed
@rtk-release-bot rtk-release-bot Bot mentioned this pull request Sep 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(init): global Claude init fails when ~/.claude is missing

3 participants