Skip to content

fix(hooks): install Claude hooks in the config dir the CLI actually reads - #25276

Closed
paoValle wants to merge 2 commits into
stablyai:mainfrom
paoValle:fix/claude-hooks-relocated-config-dir
Closed

paoValle wants to merge 2 commits into
stablyai:mainfrom
paoValle:fix/claude-hooks-relocated-config-dir

Conversation

@paoValle

@paoValle paoValle commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Author: @paolovalletta_

ELI5

Claude can be told to keep its config in a directory other than ~/.claude. Orca ignored that and kept writing its hooks into ~/.claude, where Claude never looks. The hooks therefore never ran, and every feature that depends on them — the reported one is "Stay awake while an agent is working" — saw nothing happening.

What Changed

Before: with CLAUDE_CONFIG_DIR set in the environment Orca runs in, its Claude hooks were written to ~/.claude/settings.json. The CLI reads settings.json from the directory that variable names, so the managed hooks were never loaded and no hook event ever reached Orca.

After: the hooks are written to $CLAUDE_CONFIG_DIR/settings.json, next to the settings the CLI actually reads.

Mechanism. getConfigPath() in src/main/claude/hook-settings.ts built its path as join(homedir(), settings.configDirName, 'settings.json'), hardcoding the default directory. It now resolves through defaultClaudeConfigDir(), the helper the rest of the app already uses for this variable (env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), '.claude')), for descriptors that declare one:

  • CLAUDE_HOOK_SETTINGS declares configDirEnvVar: 'CLAUDE_CONFIG_DIR'.
  • The fork descriptors (.openclaude, .qoder, .qoder-cn, .qwen, .codebuddy) declare none, so their behavior is unchanged — I could not establish that they honour this variable, and changing them without evidence would be a guess.

Two things I deliberately left alone: the managed script keeps its absolute path under the default dir (the hook command invokes it by absolute path, so a relocated settings file still works and no files move), and getRemoteConfigPath() for SSH hosts is untouched, since its config dir belongs to the remote home and reading the remote environment here is a different mechanism.

Why

The app already treats this variable as the way a Claude home is relocated — claude-config-dir-pin.ts exists to decide when to pin it, and native-chat/session-file-resolver.ts documents the rule as "Why CLAUDE_CONFIG_DIR and not just homedir()" — so the hook installer was the outlier rather than the norm. Reusing defaultClaudeConfigDir() keeps one resolution rule instead of adding a second.

Alternatives considered:

  • Read env.CLAUDE_CONFIG_DIR out of the CLI's own settings.json and follow that. This is what the reported configuration uses, and I did not implement it: the CLI resolves its config directory before it reads settings.json, so a variable declared in that file's env block most likely does not relocate where the CLI looks for its own settings (that block is applied to the sessions the CLI spawns). Implementing it on the assumption that it does would install hooks in a directory the CLI may never read. See Limitations.
  • Change the fork descriptors too. Rejected as unverified, above.
  • Leave the hooks in the default dir and point the CLI at it. Rejected: that contradicts the user's explicit configuration.

Linked Issue

Fixes #24873

#25242 is a different case and is left open. #24873 states the same defect on its own — the installer writes ~/.claude/settings.json while CLAUDE_CONFIG_DIR names another directory — and this change fixes exactly that. #25242 reports the feature failing under a configuration that declares the variable inside ~/.claude/settings.json, which this change does not cover (argument above, and in my comment on that issue). If a maintainer or the reporter can confirm the CLI relocates its own settings lookup from that block, following it is a small follow-up on top of this change.

Visual Proof

N/A

No UI change: which file the hook installer writes, and therefore whether the hooks run.

Testing

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

New src/main/claude/hook-settings.test.ts — four resolution cases (default dir, relocated dir, surrounding whitespace, a fork descriptor unaffected) plus an end-to-end case that installs through ClaudeHookService with HOME/USERPROFILE isolated to a temp dir and the variable pointing at another, then asserts the managed hooks are in that directory's settings.json and that no settings file is left in the default directory.

run result
hook-settings.test.ts with the change 5 passed
hook-settings.test.ts with only hook-settings.ts reverted 3 failed / 2 passed, e.g. expected 'C:\Users\celti\.claude\settings.json' to be 'tmp\orca-relocated-claude\settings.json'
src/main/claude/ + src/main/agent-hooks/ (248 files, 2587 tests) failure set identical to the same run on the reverted tree; the only delta is the 3 new cases failing without the fix
tsc -p config/tsconfig.node.json exit 0
pnpm tc (node + cli + web), macOS / Node 24, rebased onto current main exit 0
hook-settings.test.ts, macOS / Node 24, rebased onto current main 5 passed
oxlint on both files, check-changed-code-quality.mjs 0 findings

Those two suites carry 36 Windows-only failures that are identical with and without this change (symlink/junction creation in installer-utils, plus CRLF-sensitive expectations); I verified them A/B rather than assuming. One file (managed-hook-stdin-lifecycle.test.ts) appeared to differ in the first A/B run; it passes 3/3 in isolation both ways, so that was parallel-run interference, not this change.

Platforms tested: Windows only. The change is platform-neutral path composition — it never inspects process.platform — and the default branch is unchanged when the variable is unset.

After rebasing onto current main the same checks were re-run on macOS / Node 24 (pnpm tc, hook-settings.test.ts), because the branch had moved 63 commits. That run caught an omission in the first commit: importing claude-config-dir-pin.ts from hook-settings.ts made tc:cli fail with TS6307 until the file was listed in config/tsconfig.cli.json, which the second commit does.

AI Disclosure

Written with an AI coding agent, session model deepseek-v4.1-flash (provider opencode-go). All commands and results above were produced on this machine.

Review

  • Cross-platform: path composition only, no platform branch added. The variable is read case-sensitively on POSIX and via process.env on Windows, which is how the existing helper already behaves.
  • SSH / remote / local: local install only; getRemoteConfigPath and the SFTP path are untouched, so remote installs keep their current behavior.
  • Agent and integration compatibility: this is the Claude Code hook installer. The fork CLIs sharing the descriptor are unchanged by construction (they declare no variable). No hook event surface changed — the same managed hooks, written to the directory the CLI reads.
  • Performance risk: none; path resolution runs at install/status time.
  • UI quality: not applicable.
  • Security: this decides where Orca writes a settings file, so it is worth stating: the target is the directory the CLI itself is configured to use, it is created with the same mkdirSync(dir, { recursive: true }) the writer already used, the atomic temp+rename write is unchanged, and Orca only ever merges its own managed hook entries (applyManagedHooks) into that file. No new input, network, or credential path. The write does not follow the launch-time pinning rules in claude-config-dir-pin.ts, which exist to keep a child's credential identity stable — those are about the env given to a launched CLI, not about where its settings live.
  • Self-review: checked every caller of getConfigPath (hook-service.ts getStatus/install/one more), and that writeHooksJson creates the target directory, so a relocated directory that does not exist yet is not an error path.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Limitations: the reported configuration (variable declared in the CLI's own settings.json env block) is not fixed, for the reason in Linked Issue. I could not test it end to end either: it needs a Claude CLI configured that way, and this machine has no such setup. What I could test — that the installer follows the variable when it is in the environment — is covered end to end.
  • Account-scoped homes: Orca pins the variable per launched session (claudeConfigDirEnvPatch), but the hook service is one global instance installed with Orca's own environment, so an account's home still is not where hooks land. That is a separate design question (per-account installs) and I have not touched it.
  • Backwards compatibility: for a default setup the resolved path is byte-identical to before.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass — typecheck:node, oxlint and the changed-code gate pass; the full suite cannot run here (ensure-native-runtime needs MSVC); CI will cover

@coderabbitai

coderabbitai Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 5d157ebd-9687-4b2a-9504-9fd7b39f9d52
📥 Commits

Reviewing files that changed from the base of the PR and between 5da5445 and 92ea701.

📒 Files selected for processing (1)
  • config/tsconfig.cli.json

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The hook settings type now supports configDirEnvVar, and Claude hook settings set it to CLAUDE_CONFIG_DIR. Path resolution uses defaultClaudeConfigDir() when that variable is configured. Tests cover default and relocated settings paths, unchanged OpenClaude path behavior, and hook installation to a relocated settings file.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 92ea7

Hook setup follows CLAUDE_CONFIG_DIR supplied by Orca's environment, while unset values retain the default location. The separate settings-file-only scenario remains unconfirmed, but no merge-blocking failure is established for this change.

Architecture Summary

Architecture risk: 🔵 Low · up to 92ea7

The change affects 2 systems.

Changed systems: src, config

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — src (service) was modified; 2 changed files map to changed impact.
  • observed — config (service) was modified; 1 changed file maps to changed impact.

Before / after behavior

  • observed — Modified behavior in src/main/claude/hook-settings.test.ts: Added test setup with filesystem and temporary-directory utilities, an Electron app mock, imports for the hook service and settings helpers, and a relocated-directory fixture.
  • observed — Modified behavior in src/main/claude/hook-settings.test.ts: Added getConfigPath tests for the default Claude directory when the environment variable is empty, a relocated directory with and without surrounding whitespace, and the unchanged OpenClaude directory when the variable is set.
  • observed — Modified behavior in src/main/claude/hook-settings.test.ts: Added an installation test using temporary home and relocated directories. It checks that installation reports installed, writes hooks to the relocated settings path, and leaves no settings file in the default Claude directory; setup and teardown stub and restore environment variables and remove the temporary directories.
  • observed — Modified behavior in src/main/claude/hook-settings.ts: Imports defaultClaudeConfigDir for resolving Claude’s configured settings directory.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Issue #25242 requires “Stay awake while an agent is working” to work when CLAUDE_CONFIG_DIR is declared in ~/.claude/settings.json. The change resolves the hook path from `defaultClaudeConfigDir()… Implement support for the #25242 settings-file env configuration and add automated coverage that verifies the hook-driven feature works with that configuration.
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 2 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The hook path change and its tests address the reported hook failure. The TypeScript project-file update supports the new helper import. The described changes do not establish any unrelated implementa…
Title check ✅ Passed The title clearly and concisely identifies the main change: installing Claude hooks in the configuration directory used by the CLI.
Description check ✅ Passed The description covers the required sections, explains the behavior change and its limits, identifies the linked issue, and reports testing and review details. It also marks visual proof as not applic…
Full details: Linked Issues check

Explanation

Issue #25242 requires “Stay awake while an agent is working” to work when CLAUDE_CONFIG_DIR is declared in ~/.claude/settings.json. The change resolves the hook path from defaultClaudeConfigDir(), which uses the process environment. The PR states that the settings-file env configuration remains unsupported and untested. The tests cover process-environment relocation, not the issue’s reported configuration.

Full details: Docstring Coverage

Explanation

Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 2 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

  • Claude hook config path now follows CLAUDE_CONFIG_DIR. getConfigPath() routes through a new hookConfigDir(); descriptors declaring configDirEnvVar (only CLAUDE_HOOK_SETTINGS) resolve via defaultClaudeConfigDir(), so the managed hooks land in $CLAUDE_CONFIG_DIR/settings.json. The fork CLIs (.openclaude, .qoder, .qoder-cn, .qwen, .codebuddy) declare none and keep join(homedir(), configDirName) — unchanged.
  • New hook-settings.test.ts. Four resolution cases (default dir, relocated dir, surrounding whitespace, fork unaffected) plus an end-to-end ClaudeHookService().install() that pins HOME/USERPROFILE to a temp dir, points the variable at another, and asserts the managed hooks land in that dir's settings.json with nothing left in the default one.

I verified the load-bearing external contract against the official Claude Code docs: CLAUDE_CONFIG_DIR overrides the config directory, and settings/hooks are read from <dir>/settings.json (settings, env-vars). I also checked that the repo's global vitest home guard already clears an inherited CLAUDE_CONFIG_DIR, so existing ~/.claude/settings.json hook tests stay hermetic. The remote (getRemoteConfigPath) and managed-script paths are untouched as the PR describes, and ClaudeRuntimePathResolver pins the same inherited dir for structured sessions, so the default-account case is now consistent. The reported in-settings.json env-block configuration remains a documented, out-of-scope follow-up.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

…eads

`getConfigPath` joined `homedir()` with `.claude` unconditionally, so when
`CLAUDE_CONFIG_DIR` named another dir the managed hooks were written to
`~/.claude/settings.json`, which the CLI never opens. Every hook-driven
feature then reads inactive; the reported symptom is "Stay awake while an
agent is working" staying Inactive (stablyai#25242).

Orca already launches an account whose home is not the CLI default by
pinning exactly that variable (`claudeConfigDirEnvPatch`), so those
sessions ran no hooks at all.

Resolve the dir through `defaultClaudeConfigDir()`, the helper the rest of
the app uses for this variable, and only for the descriptors that declare
one. The fork CLIs keep their own dir names and declare no variable, so
their behavior is unchanged. The managed script keeps its absolute path in
the default dir, and `writeHooksJson` still creates the target directory.

Verified: with the variable set, an install now writes the managed hooks
into that dir's settings.json and leaves no settings file in the default
dir. Without the production change the new cases fail with
`expected 'C:\Users\celti\.claude\settings.json' to be
'tmp\orca-relocated-claude\settings.json'`.
@paoValle
paoValle force-pushed the fix/claude-hooks-relocated-config-dir branch from 495ffff to 5da5445 Compare October 5, 2026 07:30
config/tsconfig.cli.json lists the main-process files the CLI project compiles,
so the new import made tc:cli fail with TS6307 until the helper was listed.
@paoValle

paoValle commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (the branch was 63 commits behind) and re-verified on macOS / Node 24: pnpm tc (node + cli + web) exit 0, hook-settings.test.ts 5 passed.

The rebase run caught a real omission in the first commit: importing claude-config-dir-pin.ts from hook-settings.ts breaks tc:cli with TS6307 until that file is listed in config/tsconfig.cli.json. That is the second commit, and the body now records it.

Two body fixes as well:

Finally, for full disclosure: #24894 by @ggbdpq fixes the same issue and is two days older. I have compared them on that PR and I am happy to close this one in its favour — see my comment there.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

  • Registered claude-config-dir-pin.ts in the CLI TypeScript project. The new defaultClaudeConfigDir import in hook-settings.ts left the helper outside config/tsconfig.cli.json's include, so tc:cli failed with TS6307; the commit lists it alongside the other src/main/claude/* entries. tsconfig.tc.cli.json extends tsconfig.cli.json, so this is the correct project, and claude-config-dir-pin.ts imports only node:os/node:path, so nothing further is pulled in. I ran tsc --noEmit -p config/tsconfig.tc.cli.json and it exits 0.

The underlying hook-path change from the previous review is unchanged, and the prior review found no issues. The reported in-settings.json env-block configuration remains a documented, out-of-scope follow-up.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@paoValle

paoValle commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Closing this in favour of #24894. Two reasons, both of them the point rather than courtesy:

The one thing this branch carried that the other does not is the discriminator: this one declares configDirEnvVar: 'CLAUDE_CONFIG_DIR' on the hook descriptor and switches on that, so the fork descriptors (.openclaude, .qoder, .qoder-cn, .qwen, .codebuddy) are unaffected by construction instead of by matching the .claude directory name. That is a preference, not a requirement — it is a smaller structural difference than it looks — and I am not going to hold two PRs open over it. If a reviewer would rather have the descriptor field, take it from this branch; the branch stays up.

Nothing here is abandoned: the fix for #24873 is in #24894, and the other half of what I traced — the frame that reaches the chat as claude · message:system:init — is #25477 in #25514.

@paoValle paoValle closed this Oct 5, 2026
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.

[Bug]: Claude agent-status hooks are written to ~/.claude/settings.json even when CLAUDE_CONFIG_DIR is set

1 participant