diff --git a/CLAUDE.md b/CLAUDE.md
index 6a26da4..b8db2dd 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -57,6 +57,10 @@ The project has dual runtime compatibility - works with both Bun and Node.js:
- Manages flex separator expansion
- **powerline.ts**: Powerline font detection and installation
- **claude-settings.ts**: Integration with Claude Code settings.json
+ - Respects `CLAUDE_CONFIG_DIR` environment variable with fallback to `~/.claude`
+ - Provides installation command constants (NPM, BUNX, self-managed)
+ - Detects installation status and manages settings.json updates
+ - Validates config directory paths with proper error handling
- **colors.ts**: Color definitions and ANSI code mapping
### Widgets (src/widgets/)
diff --git a/README.md b/README.md
index f5ef304..0b879fe 100644
--- a/README.md
+++ b/README.md
@@ -125,6 +125,7 @@
- **🖥️ Interactive TUI** - Built-in configuration interface using React/Ink
- **⚙️ Global Options** - Apply consistent formatting across all widgets (padding, separators, bold, background)
- **🚀 Cross-platform** - Works seamlessly with both Bun and Node.js
+- **🔧 Flexible Configuration** - Supports custom Claude Code config directory via `CLAUDE_CONFIG_DIR` environment variable
- **📏 Smart Width Detection** - Automatically adapts to terminal width with flex separators
- **⚡ Zero Config** - Sensible defaults that work out of the box
@@ -155,6 +156,15 @@ The interactive configuration tool provides a terminal UI where you can:
> 💡 **Tip:** Your settings are automatically saved to `~/.config/ccstatusline/settings.json`
+> 🔧 **Custom Claude Config:** If your Claude Code configuration is in a non-standard location, set the `CLAUDE_CONFIG_DIR` environment variable:
+> ```bash
+> # Linux/macOS
+> export CLAUDE_CONFIG_DIR=/custom/path/to/.claude
+>
+> # Windows PowerShell
+> $env:CLAUDE_CONFIG_DIR="C:\custom\path\.claude"
+> ```
+
---
## 🪟 Windows Support
@@ -294,7 +304,11 @@ For the best experience, configure Windows Terminal with these recommended setti
#### Claude Code Integration
Configure ccstatusline in your Claude Code settings:
-**For Bun users** (Windows: `%USERPROFILE%\.claude\settings.json`):
+**Settings Location:**
+- Default: `~/.claude/settings.json` (Windows: `%USERPROFILE%\.claude\settings.json`)
+- Custom: Set `CLAUDE_CONFIG_DIR` environment variable to use a different directory
+
+**For Bun users**:
```json
{
"statusLine": "bunx ccstatusline@latest"
@@ -308,6 +322,8 @@ Configure ccstatusline in your Claude Code settings:
}
```
+> 💡 **Custom Config Directory:** If you use a non-standard Claude Code configuration directory, set the `CLAUDE_CONFIG_DIR` environment variable before running ccstatusline. The tool will automatically detect and use your custom location.
+
### Performance on Windows
ccstatusline is optimized for Windows performance:
@@ -561,7 +577,7 @@ ccstatusline/
│ │ ├── renderer.ts # Core rendering logic
│ │ ├── powerline.ts # Powerline font utilities
│ │ ├── colors.ts # Color definitions
-│ │ └── claude-settings.ts # Claude Code integration
+│ │ └── claude-settings.ts # Claude Code integration (supports CLAUDE_CONFIG_DIR)
│ └── types/ # TypeScript type definitions
│ ├── Settings.ts
│ ├── Widget.ts
diff --git a/src/tui/App.tsx b/src/tui/App.tsx
index 19609f2..7cbef15 100644
--- a/src/tui/App.tsx
+++ b/src/tui/App.tsx
@@ -8,6 +8,7 @@ import {
} from 'ink';
import Gradient from 'ink-gradient';
import React, {
+ useCallback,
useEffect,
useState
} from 'react';
@@ -15,6 +16,8 @@ import React, {
import type { Settings } from '../types/Settings';
import type { WidgetItem } from '../types/Widget';
import {
+ CCSTATUSLINE_COMMANDS,
+ getClaudeSettingsPath,
getExistingStatusLine,
installStatusLine,
isBunxAvailable,
@@ -129,6 +132,41 @@ export const App: React.FC = () => {
}
});
+ const handleInstallSelection = useCallback((command: string, displayName: string, useBunx: boolean) => {
+ void getExistingStatusLine().then((existing) => {
+ const isAlreadyInstalled = [CCSTATUSLINE_COMMANDS.NPM, CCSTATUSLINE_COMMANDS.BUNX, CCSTATUSLINE_COMMANDS.SELF_MANAGED].includes(existing ?? '');
+ let message: string;
+
+ if (existing && !isAlreadyInstalled) {
+ message = `This will modify ${getClaudeSettingsPath()}\n\nA status line is already configured: "${existing}"\nReplace it with ${command}?`;
+ } else if (isAlreadyInstalled) {
+ message = `ccstatusline is already installed in ${getClaudeSettingsPath()}\nUpdate it with ${command}?`;
+ } else {
+ message = `This will modify ${getClaudeSettingsPath()} to add ccstatusline with ${displayName}.\nContinue?`;
+ }
+
+ setConfirmDialog({
+ message,
+ action: async () => {
+ await installStatusLine(useBunx);
+ setIsClaudeInstalled(true);
+ setExistingStatusLine(command);
+ setScreen('main');
+ setConfirmDialog(null);
+ }
+ });
+ setScreen('confirm');
+ });
+ }, []);
+
+ const handleNpxInstall = useCallback(() => {
+ handleInstallSelection(CCSTATUSLINE_COMMANDS.NPM, 'npx', false);
+ }, [handleInstallSelection]);
+
+ const handleBunxInstall = useCallback(() => {
+ handleInstallSelection(CCSTATUSLINE_COMMANDS.BUNX, 'bunx', true);
+ }, [handleInstallSelection]);
+
if (!settings) {
return Loading settings...;
}
@@ -137,7 +175,7 @@ export const App: React.FC = () => {
if (isClaudeInstalled) {
// Uninstall
setConfirmDialog({
- message: 'This will remove ccstatusline from ~/.claude/settings.json. Continue?',
+ message: `This will remove ccstatusline from ${getClaudeSettingsPath()}. Continue?`,
action: async () => {
await uninstallStatusLine();
setIsClaudeInstalled(false);
@@ -376,58 +414,8 @@ export const App: React.FC = () => {
{
- void getExistingStatusLine().then((existing) => {
- const isAlreadyInstalled = ['npx -y ccstatusline@latest', 'bunx -y ccstatusline@latest'].includes(existing ?? '');
- let message: string;
-
- if (existing && !isAlreadyInstalled) {
- message = `This will modify ~/.claude/settings.json\n\nA status line is already configured: "${existing}"\nReplace it with npx -y ccstatusline@latest?`;
- } else if (isAlreadyInstalled) {
- message = 'ccstatusline is already installed in ~/.claude/settings.json\nUpdate it with npx -y ccstatusline@latest?';
- } else {
- message = 'This will modify ~/.claude/settings.json to add ccstatusline with npx.\nContinue?';
- }
-
- setConfirmDialog({
- message,
- action: async () => {
- await installStatusLine(false);
- setIsClaudeInstalled(true);
- setExistingStatusLine('npx -y ccstatusline@latest');
- setScreen('main');
- setConfirmDialog(null);
- }
- });
- setScreen('confirm');
- });
- }}
- onSelectBunx={() => {
- void getExistingStatusLine().then((existing) => {
- const isAlreadyInstalled = ['npx -y ccstatusline@latest', 'bunx -y ccstatusline@latest'].includes(existing ?? '');
- let message: string;
-
- if (existing && !isAlreadyInstalled) {
- message = `This will modify ~/.claude/settings.json\n\nA status line is already configured: "${existing}"\nReplace it with bunx -y ccstatusline@latest?`;
- } else if (isAlreadyInstalled) {
- message = 'ccstatusline is already installed in ~/.claude/settings.json\nUpdate it with bunx -y ccstatusline@latest?';
- } else {
- message = 'This will modify ~/.claude/settings.json to add ccstatusline with bunx.\nContinue?';
- }
-
- setConfirmDialog({
- message,
- action: async () => {
- await installStatusLine(true);
- setIsClaudeInstalled(true);
- setExistingStatusLine('bunx -y ccstatusline@latest');
- setScreen('main');
- setConfirmDialog(null);
- }
- });
- setScreen('confirm');
- });
- }}
+ onSelectNpx={handleNpxInstall}
+ onSelectBunx={handleBunxInstall}
onCancel={() => {
setScreen('main');
}}
diff --git a/src/tui/components/InstallMenu.tsx b/src/tui/components/InstallMenu.tsx
index 3a7cbf9..5160b26 100644
--- a/src/tui/components/InstallMenu.tsx
+++ b/src/tui/components/InstallMenu.tsx
@@ -5,6 +5,8 @@ import {
} from 'ink';
import React, { useState } from 'react';
+import { getClaudeSettingsPath } from '../../utils/claude-settings';
+
export interface InstallMenuProps {
bunxAvailable: boolean;
existingStatusLine: string | null;
@@ -95,7 +97,9 @@ export const InstallMenu: React.FC = ({
- The selected command will be written to ~/.claude/settings.json
+ The selected command will be written to
+ {' '}
+ {getClaudeSettingsPath()}
diff --git a/src/tui/components/LineSelector.tsx b/src/tui/components/LineSelector.tsx
index da0e0e9..1733337 100644
--- a/src/tui/components/LineSelector.tsx
+++ b/src/tui/components/LineSelector.tsx
@@ -73,10 +73,10 @@ const LineSelector: React.FC = ({
const powerlineEnabled = settings ? settings.powerline.enabled : false;
const powerlineTheme = settings ? settings.powerline.theme : undefined;
const isThemeManaged
- = blockIfPowerlineActive
- && powerlineEnabled
- && powerlineTheme
- && powerlineTheme !== 'custom';
+ = blockIfPowerlineActive
+ && powerlineEnabled
+ && powerlineTheme
+ && powerlineTheme !== 'custom';
// Handle keyboard input
useInput((input, key) => {
@@ -153,9 +153,9 @@ const LineSelector: React.FC = ({
if (showDeleteDialog && selectedLine) {
const suffix
- = selectedLine.length > 0
- ? pluralize('widget', selectedLine.length, true)
- : 'empty';
+ = selectedLine.length > 0
+ ? pluralize('widget', selectedLine.length, true)
+ : 'empty';
return (
diff --git a/src/utils/claude-settings.ts b/src/utils/claude-settings.ts
index 2fdc1a6..43c4620 100644
--- a/src/utils/claude-settings.ts
+++ b/src/utils/claude-settings.ts
@@ -13,32 +13,89 @@ const readFile = fs.promises.readFile;
const writeFile = fs.promises.writeFile;
const mkdir = fs.promises.mkdir;
-const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
+export const CCSTATUSLINE_COMMANDS = {
+ NPM: 'npx -y ccstatusline@latest',
+ BUNX: 'bunx -y ccstatusline@latest',
+ SELF_MANAGED: 'ccstatusline'
+};
+
+/**
+ * Determines the Claude config directory, checking CLAUDE_CONFIG_DIR environment variable first,
+ * then falling back to the default ~/.claude directory.
+ */
+export function getClaudeConfigDir(): string {
+ const envConfigDir = process.env.CLAUDE_CONFIG_DIR;
+
+ if (envConfigDir) {
+ try {
+ // Validate that the path is absolute and reasonable
+ const resolvedPath = path.resolve(envConfigDir);
+
+ // Check if directory exists or can be created
+ if (fs.existsSync(resolvedPath)) {
+ const stats = fs.statSync(resolvedPath);
+ if (stats.isDirectory()) {
+ return resolvedPath;
+ }
+ } else {
+ // Directory doesn't exist yet, but we can try to use it
+ // (mkdir will be called later when saving)
+ return resolvedPath;
+ }
+ } catch {
+ // Fall through to default on any error
+ }
+ }
+
+ // Default fallback
+ return path.join(os.homedir(), '.claude');
+}
+
+/**
+ * Gets the full path to the Claude settings.json file.
+ */
+export function getClaudeSettingsPath(): string {
+ return path.join(getClaudeConfigDir(), 'settings.json');
+}
export async function loadClaudeSettings(): Promise {
try {
- if (!fs.existsSync(CLAUDE_SETTINGS_PATH)) {
+ const settingsPath = getClaudeSettingsPath();
+ if (!fs.existsSync(settingsPath)) {
return {};
}
- const content = await readFile(CLAUDE_SETTINGS_PATH, 'utf-8');
+ const content = await readFile(settingsPath, 'utf-8');
return JSON.parse(content) as ClaudeSettings;
} catch {
return {};
}
}
-export async function saveClaudeSettings(settings: ClaudeSettings): Promise {
- const dir = path.dirname(CLAUDE_SETTINGS_PATH);
+export async function saveClaudeSettings(
+ settings: ClaudeSettings
+): Promise {
+ const settingsPath = getClaudeSettingsPath();
+ const dir = path.dirname(settingsPath);
await mkdir(dir, { recursive: true });
- await writeFile(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf-8');
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
}
export async function isInstalled(): Promise {
const settings = await loadClaudeSettings();
// Check if command is either npx or bunx version AND padding is 0 (or undefined for new installs)
- const validCommands = ['npx -y ccstatusline@latest', 'bunx -y ccstatusline@latest'];
- return validCommands.includes(settings.statusLine?.command ?? '')
- && (settings.statusLine?.padding === 0 || settings.statusLine?.padding === undefined);
+ const validCommands = [
+ // Default autoinstalled npm command
+ CCSTATUSLINE_COMMANDS.NPM,
+ // Default autoinstalled bunx command
+ CCSTATUSLINE_COMMANDS.BUNX,
+ // Self managed installation command
+ CCSTATUSLINE_COMMANDS.SELF_MANAGED
+ ];
+ return (
+ validCommands.includes(settings.statusLine?.command ?? '')
+ && (settings.statusLine?.padding === 0
+ || settings.statusLine?.padding === undefined)
+ );
}
export function isBunxAvailable(): boolean {
@@ -58,7 +115,9 @@ export async function installStatusLine(useBunx = false): Promise {
// Update settings with our status line (confirmation already handled in TUI)
settings.statusLine = {
type: 'command',
- command: useBunx ? 'bunx -y ccstatusline@latest' : 'npx -y ccstatusline@latest',
+ command: useBunx
+ ? CCSTATUSLINE_COMMANDS.BUNX
+ : CCSTATUSLINE_COMMANDS.NPM,
padding: 0
};