fix: honor CLAUDE_CONFIG_DIR environment variable (#261)
Fixes #255 - Add getClaudeConfigDir() utility function that respects CLAUDE_CONFIG_DIR env var - Update all hardcoded ~/.claude paths to use the new utility - Add comprehensive tests for getClaudeConfigDir() - Maintain backward compatibility with default ~/.claude when env var is not set Files updated: - src/shared/claude-config-dir.ts (new utility) - src/shared/claude-config-dir.test.ts (tests) - src/hooks/claude-code-hooks/config.ts - src/hooks/claude-code-hooks/todo.ts - src/hooks/claude-code-hooks/transcript.ts - src/features/claude-code-command-loader/loader.ts - src/features/claude-code-agent-loader/loader.ts - src/features/claude-code-skill-loader/loader.ts - src/features/claude-code-mcp-loader/loader.ts - src/tools/session-manager/constants.ts - src/tools/slashcommand/tools.ts Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { existsSync, readdirSync, readFileSync } from "fs"
|
||||
import { homedir } from "os"
|
||||
import { join, basename } from "path"
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import { parseFrontmatter } from "../../shared/frontmatter"
|
||||
import { isMarkdownFile } from "../../shared/file-utils"
|
||||
import { getClaudeConfigDir } from "../../shared"
|
||||
import type { AgentScope, AgentFrontmatter, LoadedAgent } from "./types"
|
||||
|
||||
function parseToolsConfig(toolsStr?: string): Record<string, boolean> | undefined {
|
||||
@@ -68,7 +68,7 @@ function loadAgentsFromDir(agentsDir: string, scope: AgentScope): LoadedAgent[]
|
||||
}
|
||||
|
||||
export function loadUserAgents(): Record<string, AgentConfig> {
|
||||
const userAgentsDir = join(homedir(), ".claude", "agents")
|
||||
const userAgentsDir = join(getClaudeConfigDir(), "agents")
|
||||
const agents = loadAgentsFromDir(userAgentsDir, "user")
|
||||
|
||||
const result: Record<string, AgentConfig> = {}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { existsSync, readdirSync, readFileSync } from "fs"
|
||||
import { homedir } from "os"
|
||||
import { join, basename } from "path"
|
||||
import { parseFrontmatter } from "../../shared/frontmatter"
|
||||
import { sanitizeModelField } from "../../shared/model-sanitizer"
|
||||
import { isMarkdownFile } from "../../shared/file-utils"
|
||||
import { getClaudeConfigDir } from "../../shared"
|
||||
import type { CommandScope, CommandDefinition, CommandFrontmatter, LoadedCommand } from "./types"
|
||||
|
||||
function loadCommandsFromDir(commandsDir: string, scope: CommandScope): LoadedCommand[] {
|
||||
@@ -68,7 +68,7 @@ function commandsToRecord(commands: LoadedCommand[]): Record<string, CommandDefi
|
||||
}
|
||||
|
||||
export function loadUserCommands(): Record<string, CommandDefinition> {
|
||||
const userCommandsDir = join(homedir(), ".claude", "commands")
|
||||
const userCommandsDir = join(getClaudeConfigDir(), "commands")
|
||||
const commands = loadCommandsFromDir(userCommandsDir, "user")
|
||||
return commandsToRecord(commands)
|
||||
}
|
||||
@@ -80,6 +80,7 @@ export function loadProjectCommands(): Record<string, CommandDefinition> {
|
||||
}
|
||||
|
||||
export function loadOpencodeGlobalCommands(): Record<string, CommandDefinition> {
|
||||
const { homedir } = require("os")
|
||||
const opencodeCommandsDir = join(homedir(), ".config", "opencode", "command")
|
||||
const commands = loadCommandsFromDir(opencodeCommandsDir, "opencode")
|
||||
return commandsToRecord(commands)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { existsSync } from "fs"
|
||||
import { homedir } from "os"
|
||||
import { join } from "path"
|
||||
import { getClaudeConfigDir } from "../../shared"
|
||||
import type {
|
||||
ClaudeCodeMcpConfig,
|
||||
LoadedMcpServer,
|
||||
@@ -16,11 +16,11 @@ interface McpConfigPath {
|
||||
}
|
||||
|
||||
function getMcpConfigPaths(): McpConfigPath[] {
|
||||
const home = homedir()
|
||||
const claudeConfigDir = getClaudeConfigDir()
|
||||
const cwd = process.cwd()
|
||||
|
||||
return [
|
||||
{ path: join(home, ".claude", ".mcp.json"), scope: "user" },
|
||||
{ path: join(claudeConfigDir, ".mcp.json"), scope: "user" },
|
||||
{ path: join(cwd, ".mcp.json"), scope: "project" },
|
||||
{ path: join(cwd, ".claude", ".mcp.json"), scope: "local" },
|
||||
]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { existsSync, readdirSync, readFileSync } from "fs"
|
||||
import { homedir } from "os"
|
||||
import { join } from "path"
|
||||
import { parseFrontmatter } from "../../shared/frontmatter"
|
||||
import { sanitizeModelField } from "../../shared/model-sanitizer"
|
||||
import { resolveSymlink } from "../../shared/file-utils"
|
||||
import { getClaudeConfigDir } from "../../shared"
|
||||
import type { CommandDefinition } from "../claude-code-command-loader/types"
|
||||
import type { SkillScope, SkillMetadata, LoadedSkillAsCommand } from "./types"
|
||||
|
||||
@@ -68,7 +68,7 @@ $ARGUMENTS
|
||||
}
|
||||
|
||||
export function loadUserSkillsAsCommands(): Record<string, CommandDefinition> {
|
||||
const userSkillsDir = join(homedir(), ".claude", "skills")
|
||||
const userSkillsDir = join(getClaudeConfigDir(), "skills")
|
||||
const skills = loadSkillsFromDir(userSkillsDir, "user")
|
||||
return skills.reduce((acc, skill) => {
|
||||
acc[skill.name] = skill.definition
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { homedir } from "os"
|
||||
import { join } from "path"
|
||||
import { existsSync } from "fs"
|
||||
import { getClaudeConfigDir } from "../../shared"
|
||||
import type { ClaudeHooksConfig, HookMatcher, HookCommand } from "./types"
|
||||
|
||||
interface RawHookMatcher {
|
||||
@@ -44,9 +44,9 @@ function normalizeHooksConfig(raw: RawClaudeHooksConfig): ClaudeHooksConfig {
|
||||
}
|
||||
|
||||
export function getClaudeSettingsPaths(customPath?: string): string[] {
|
||||
const home = homedir()
|
||||
const claudeConfigDir = getClaudeConfigDir()
|
||||
const paths = [
|
||||
join(home, ".claude", "settings.json"),
|
||||
join(claudeConfigDir, "settings.json"),
|
||||
join(process.cwd(), ".claude", "settings.json"),
|
||||
join(process.cwd(), ".claude", "settings.local.json"),
|
||||
]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { join } from "path"
|
||||
import { mkdirSync, writeFileSync, readFileSync, existsSync, unlinkSync } from "fs"
|
||||
import { homedir } from "os"
|
||||
import { getClaudeConfigDir } from "../../shared"
|
||||
import type { TodoFile, TodoItem, ClaudeCodeTodoItem } from "./types"
|
||||
|
||||
const TODO_DIR = join(homedir(), ".claude", "todos")
|
||||
const TODO_DIR = join(getClaudeConfigDir(), "todos")
|
||||
|
||||
export function getTodoPath(sessionId: string): string {
|
||||
return join(TODO_DIR, `${sessionId}-agent-${sessionId}.json`)
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
/**
|
||||
* Transcript Manager
|
||||
* Creates and manages Claude Code compatible transcript files
|
||||
*/
|
||||
import { join } from "path"
|
||||
import { mkdirSync, appendFileSync, existsSync, writeFileSync, unlinkSync } from "fs"
|
||||
import { homedir, tmpdir } from "os"
|
||||
import { tmpdir } from "os"
|
||||
import { randomUUID } from "crypto"
|
||||
import type { TranscriptEntry } from "./types"
|
||||
import { transformToolName } from "../../shared/tool-name"
|
||||
import { getClaudeConfigDir } from "../../shared"
|
||||
|
||||
const TRANSCRIPT_DIR = join(homedir(), ".claude", "transcripts")
|
||||
const TRANSCRIPT_DIR = join(getClaudeConfigDir(), "transcripts")
|
||||
|
||||
export function getTranscriptPath(sessionId: string): string {
|
||||
return join(TRANSCRIPT_DIR, `${sessionId}.jsonl`)
|
||||
|
||||
60
src/shared/claude-config-dir.test.ts
Normal file
60
src/shared/claude-config-dir.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { getClaudeConfigDir } from "./claude-config-dir"
|
||||
|
||||
describe("getClaudeConfigDir", () => {
|
||||
let originalEnv: string | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = process.env.CLAUDE_CONFIG_DIR
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalEnv !== undefined) {
|
||||
process.env.CLAUDE_CONFIG_DIR = originalEnv
|
||||
} else {
|
||||
delete process.env.CLAUDE_CONFIG_DIR
|
||||
}
|
||||
})
|
||||
|
||||
test("returns CLAUDE_CONFIG_DIR when env var is set", () => {
|
||||
process.env.CLAUDE_CONFIG_DIR = "/custom/claude/path"
|
||||
|
||||
const result = getClaudeConfigDir()
|
||||
|
||||
expect(result).toBe("/custom/claude/path")
|
||||
})
|
||||
|
||||
test("returns ~/.claude when env var is not set", () => {
|
||||
delete process.env.CLAUDE_CONFIG_DIR
|
||||
|
||||
const result = getClaudeConfigDir()
|
||||
|
||||
expect(result).toBe(join(homedir(), ".claude"))
|
||||
})
|
||||
|
||||
test("returns ~/.claude when env var is empty string", () => {
|
||||
process.env.CLAUDE_CONFIG_DIR = ""
|
||||
|
||||
const result = getClaudeConfigDir()
|
||||
|
||||
expect(result).toBe(join(homedir(), ".claude"))
|
||||
})
|
||||
|
||||
test("handles absolute paths with trailing slash", () => {
|
||||
process.env.CLAUDE_CONFIG_DIR = "/custom/path/"
|
||||
|
||||
const result = getClaudeConfigDir()
|
||||
|
||||
expect(result).toBe("/custom/path/")
|
||||
})
|
||||
|
||||
test("handles relative paths", () => {
|
||||
process.env.CLAUDE_CONFIG_DIR = "./my-claude-config"
|
||||
|
||||
const result = getClaudeConfigDir()
|
||||
|
||||
expect(result).toBe("./my-claude-config")
|
||||
})
|
||||
})
|
||||
11
src/shared/claude-config-dir.ts
Normal file
11
src/shared/claude-config-dir.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
|
||||
export function getClaudeConfigDir(): string {
|
||||
const envConfigDir = process.env.CLAUDE_CONFIG_DIR
|
||||
if (envConfigDir) {
|
||||
return envConfigDir
|
||||
}
|
||||
|
||||
return join(homedir(), ".claude")
|
||||
}
|
||||
@@ -13,3 +13,4 @@ export * from "./dynamic-truncator"
|
||||
export * from "./config-path"
|
||||
export * from "./data-path"
|
||||
export * from "./config-errors"
|
||||
export * from "./claude-config-dir"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { join } from "node:path"
|
||||
import { homedir } from "node:os"
|
||||
import { getOpenCodeStorageDir } from "../../shared/data-path"
|
||||
import { getClaudeConfigDir } from "../../shared"
|
||||
|
||||
export const OPENCODE_STORAGE = getOpenCodeStorageDir()
|
||||
export const MESSAGE_STORAGE = join(OPENCODE_STORAGE, "message")
|
||||
export const PART_STORAGE = join(OPENCODE_STORAGE, "part")
|
||||
export const TODO_DIR = join(homedir(), ".claude", "todos")
|
||||
export const TRANSCRIPT_DIR = join(homedir(), ".claude", "transcripts")
|
||||
export const TODO_DIR = join(getClaudeConfigDir(), "todos")
|
||||
export const TRANSCRIPT_DIR = join(getClaudeConfigDir(), "transcripts")
|
||||
export const SESSION_LIST_DESCRIPTION = `List all OpenCode sessions with optional filtering.
|
||||
|
||||
Returns a list of available session IDs with metadata including message count, date range, and agents used.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
import { existsSync, readdirSync, readFileSync } from "fs"
|
||||
import { homedir } from "os"
|
||||
import { join, basename, dirname } from "path"
|
||||
import { parseFrontmatter, resolveCommandsInText, resolveFileReferencesInText, sanitizeModelField } from "../../shared"
|
||||
import { isMarkdownFile } from "../../shared/file-utils"
|
||||
import { getClaudeConfigDir } from "../../shared"
|
||||
import type { CommandScope, CommandMetadata, CommandInfo } from "./types"
|
||||
|
||||
function discoverCommandsFromDir(commandsDir: string, scope: CommandScope): CommandInfo[] {
|
||||
@@ -50,7 +50,8 @@ function discoverCommandsFromDir(commandsDir: string, scope: CommandScope): Comm
|
||||
}
|
||||
|
||||
function discoverCommandsSync(): CommandInfo[] {
|
||||
const userCommandsDir = join(homedir(), ".claude", "commands")
|
||||
const { homedir } = require("os")
|
||||
const userCommandsDir = join(getClaudeConfigDir(), "commands")
|
||||
const projectCommandsDir = join(process.cwd(), ".claude", "commands")
|
||||
const opencodeGlobalDir = join(homedir(), ".config", "opencode", "command")
|
||||
const opencodeProjectDir = join(process.cwd(), ".opencode", "command")
|
||||
@@ -145,7 +146,7 @@ Commands are loaded from (priority order, highest wins):
|
||||
- .opencode/command/ (opencode-project - OpenCode project-specific commands)
|
||||
- ./.claude/commands/ (project - Claude Code project-specific commands)
|
||||
- ~/.config/opencode/command/ (opencode - OpenCode global commands)
|
||||
- ~/.claude/commands/ (user - Claude Code global commands)
|
||||
- $CLAUDE_CONFIG_DIR/commands/ or ~/.claude/commands/ (user - Claude Code global commands)
|
||||
|
||||
Each command is a markdown file with:
|
||||
- YAML frontmatter: description, argument-hint, model, agent, subtask (optional)
|
||||
|
||||
Reference in New Issue
Block a user