claude-code: add configDir option

Make the location of Claude Code's configuration files configurable.
The option defaults to `~/.claude` so existing setups are unaffected,
and exports the `CLAUDE_CONFIG_DIR` environment variable automatically
whenever the directory differs from the upstream default so the CLI
reads from the same location home-manager wrote to.

The option follows the convention established by
`programs.github-copilot-cli`: an absolute-path string with a
`defaultText` for clean documentation rendering, an `upstreamConfigDir`
constant, and a session variable wired only when the user opts out of
the upstream default.
This commit is contained in:
jiezhuzzz
2026-05-07 17:58:44 -05:00
committed by Austin Horstman
parent 3e707f5f93
commit e7a7c550e2
3 changed files with 143 additions and 26 deletions

View File

@@ -17,6 +17,8 @@ let
jsonFormat = pkgs.formats.json { };
upstreamConfigDir = "${config.home.homeDirectory}/.claude";
mkMcpServer =
server:
(removeAttrs server [ "disabled" ])
@@ -100,6 +102,22 @@ in
'';
};
configDir = mkOption {
type = lib.types.str;
default = upstreamConfigDir;
defaultText = literalExpression ''"''${config.home.homeDirectory}/.claude"'';
example = literalExpression ''"''${config.xdg.configHome}/claude"'';
description = ''
Directory holding Claude Code's configuration files.
Defaults to {file}`~/.claude`, matching the upstream
{command}`claude` CLI default. The {env}`CLAUDE_CONFIG_DIR`
environment variable is exported automatically whenever the
directory differs from this default so the CLI reads
configuration from the same location.
'';
};
settings = mkOption {
inherit (jsonFormat) type;
default = { };
@@ -167,7 +185,8 @@ in
- A path to a file containing the content
The configured content is written to
{file}`~/.claude/CLAUDE.md`.
{file}`CLAUDE.md` inside {option}`programs.claude-code.configDir`
(default {file}`~/.claude/CLAUDE.md`).
'';
example = literalExpression "./claude-memory.md";
};
@@ -223,7 +242,8 @@ in
The attribute name becomes the agent filename, and the value is either:
- Inline content as a string with frontmatter
- A path to a file containing the agent content with frontmatter
Agents are stored in .claude/agents/ directory.
Agents are stored in the {file}`agents/` subdirectory of
{option}`programs.claude-code.configDir`.
'';
example = literalExpression ''
{
@@ -248,7 +268,8 @@ in
The attribute name becomes the command filename, and the value is either:
- Inline content as a string
- A path to a file containing the command content
Commands are stored in .claude/commands/ directory.
Commands are stored in the {file}`commands/` subdirectory of
{option}`programs.claude-code.configDir`.
'';
example = literalExpression ''
{
@@ -287,7 +308,8 @@ in
description = ''
Custom hooks for Claude Code.
The attribute name becomes the hook filename, and the value is the hook script content.
Hooks are stored in .claude/hooks/ directory.
Hooks are stored in the {file}`hooks/` subdirectory of
{option}`programs.claude-code.configDir`.
'';
example = {
pre-edit = ''
@@ -307,8 +329,9 @@ in
The attribute name becomes the rule filename, and the value is either:
- Inline content as a string
- A path to a file containing the rule content
Rules are stored in .claude/rules/ directory.
All markdown files in .claude/rules/ are automatically loaded as project memory.
Rules are stored in the {file}`rules/` subdirectory of
{option}`programs.claude-code.configDir`. All markdown files in
that directory are automatically loaded as project memory.
'';
example = literalExpression ''
{
@@ -332,8 +355,10 @@ in
rulesDir = mkDirOption {
description = ''
Path to a directory containing rule files for Claude Code.
Rule files from this directory will be symlinked to .claude/rules/.
All markdown files in this directory are automatically loaded as project memory.
Rule files from this directory will be symlinked into the
{file}`rules/` subdirectory of
{option}`programs.claude-code.configDir`. All markdown files in
this directory are automatically loaded as project memory.
'';
example = literalExpression "./rules";
};
@@ -341,7 +366,9 @@ in
agentsDir = mkDirOption {
description = ''
Path to a directory containing agent files for Claude Code.
Agent files from this directory will be symlinked to .claude/agents/.
Agent files from this directory will be symlinked into the
{file}`agents/` subdirectory of
{option}`programs.claude-code.configDir`.
'';
example = literalExpression "./agents";
};
@@ -349,7 +376,9 @@ in
commandsDir = mkDirOption {
description = ''
Path to a directory containing command files for Claude Code.
Command files from this directory will be symlinked to .claude/commands/.
Command files from this directory will be symlinked into the
{file}`commands/` subdirectory of
{option}`programs.claude-code.configDir`.
'';
example = literalExpression "./commands";
};
@@ -357,7 +386,9 @@ in
hooksDir = mkDirOption {
description = ''
Path to a directory containing hook files for Claude Code.
Hook files from this directory will be symlinked to .claude/hooks/.
Hook files from this directory will be symlinked into the
{file}`hooks/` subdirectory of
{option}`programs.claude-code.configDir`.
'';
example = literalExpression "./hooks";
};
@@ -369,7 +400,9 @@ in
The value is either:
- Inline content as a string
- A path to a file
In both cases, the contents will be written to .claude/output-styles/<name>.md
In both cases, the contents will be written to
{file}`output-styles/<name>.md` inside
{option}`programs.claude-code.configDir`.
'';
example = literalExpression ''
{
@@ -401,16 +434,17 @@ in
If an attribute set is used, the attribute name becomes the
skill directory name, and the value is either:
- Inline content as a string (creates .claude/skills/<name>/SKILL.md)
- A path to a file (creates .claude/skills/<name>/SKILL.md)
- A path to a directory (creates .claude/skills/<name>/ with all files)
- Inline content as a string (creates {file}`skills/<name>/SKILL.md`)
- A path to a file (creates {file}`skills/<name>/SKILL.md`)
- A path to a directory (creates {file}`skills/<name>/` with all files)
This also accepts Nix store paths, for example a skill directory
from a package.
If a path is used, it is expected to contain one folder per
skill name, each containing a {file}`SKILL.md`. The directory is
symlinked to {file}`~/.claude/skills/`.
symlinked into the {file}`skills/` subdirectory of
{option}`programs.claude-code.configDir`.
'';
example = literalExpression ''
{
@@ -521,17 +555,19 @@ in
mkMarkdownEntries =
subdir: attrs:
lib.mapAttrs' (
name: content: nameValuePair ".claude/${subdir}/${name}.md" (mkSourceEntry content)
name: content: nameValuePair "${cfg.configDir}/${subdir}/${name}.md" (mkSourceEntry content)
) attrs;
mkTextEntries =
subdir: attrs:
lib.mapAttrs' (name: content: nameValuePair ".claude/${subdir}/${name}" { text = content; }) attrs;
lib.mapAttrs' (
name: content: nameValuePair "${cfg.configDir}/${subdir}/${name}" { text = content; }
) attrs;
mkRecursiveDirAttrs =
subdir: dir:
optionalAttrs (dir != null) {
".claude/${subdir}" = {
"${cfg.configDir}/${subdir}" = {
source = dir;
recursive = true;
};
@@ -540,12 +576,12 @@ in
mkSkillEntry =
name: content:
if isPathLikeContent content && lib.pathIsDirectory content then
nameValuePair ".claude/skills/${name}" {
nameValuePair "${cfg.configDir}/skills/${name}" {
source = content;
recursive = true;
}
else
nameValuePair ".claude/skills/${name}/SKILL.md" (
nameValuePair "${cfg.configDir}/skills/${name}/SKILL.md" (
if isPathLikeContent content then { source = content; } else { text = content; }
);
@@ -646,9 +682,13 @@ in
home = {
packages = lib.mkIf (cfg.package != null) [ cfg.finalPackage ];
sessionVariables = lib.mkIf (cfg.configDir != upstreamConfigDir) {
CLAUDE_CONFIG_DIR = cfg.configDir;
};
file = lib.mkMerge [
(lib.mkIf (cfg.settings != { } || cfg.marketplaces != { }) {
".claude/settings.json".source = jsonFormat.generate "claude-code-settings.json" (
"${cfg.configDir}/settings.json".source = jsonFormat.generate "claude-code-settings.json" (
cfg.settings
// {
"$schema" = "https://json.schemastore.org/claude-code-settings.json";
@@ -661,15 +701,15 @@ in
(
if lib.isPath cfg.context then
{
".claude/CLAUDE.md".source = cfg.context;
"${cfg.configDir}/CLAUDE.md".source = cfg.context;
}
else
(lib.mkIf (cfg.context != "") {
".claude/CLAUDE.md".text = cfg.context;
"${cfg.configDir}/CLAUDE.md".text = cfg.context;
})
)
(lib.mkIf (cfg.marketplaces != { }) {
".claude/plugins/known_marketplaces.json".source =
"${cfg.configDir}/plugins/known_marketplaces.json".source =
jsonFormat.generate "claude-code-known-marketplaces.json" (
lib.mapAttrs mkInstalledMarketplaceEntry cfg.marketplaces
);
@@ -682,7 +722,7 @@ in
(mkRecursiveDirAttrs "hooks" cfg.hooksDir)
(mkRecursiveDirAttrs "rules" cfg.rulesDir)
(lib.mkIf (isPathLikeContent cfg.skills) {
".claude/skills" = {
"${cfg.configDir}/skills" = {
source = cfg.skills;
recursive = true;
};

View File

@@ -0,0 +1,76 @@
{ config, ... }:
{
programs.claude-code = {
enable = true;
configDir = "${config.xdg.configHome}/claude";
settings = {
theme = "dark";
};
context = ''
# Custom context
'';
agents = {
reviewer = ''
---
name: reviewer
description: code reviewer
---
body
'';
};
commands = {
hello = ''
---
description: hello command
---
body
'';
};
rules = {
style = "rule body";
};
hooks = {
pre-edit = ''
#!/usr/bin/env bash
echo hi
'';
};
outputStyles = {
concise = "concise body";
};
skills = {
pdf = ''
---
name: pdf
description: pdf skill
---
body
'';
};
};
nmt.script = ''
assertPathNotExists home-files/.claude
assertFileExists home-files/.config/claude/settings.json
assertFileExists home-files/.config/claude/CLAUDE.md
assertFileExists home-files/.config/claude/agents/reviewer.md
assertFileExists home-files/.config/claude/commands/hello.md
assertFileExists home-files/.config/claude/rules/style.md
assertFileExists home-files/.config/claude/hooks/pre-edit
assertFileExists home-files/.config/claude/output-styles/concise.md
assertFileExists home-files/.config/claude/skills/pdf/SKILL.md
assertFileExists home-path/etc/profile.d/hm-session-vars.sh
assertFileRegex home-path/etc/profile.d/hm-session-vars.sh \
'export CLAUDE_CONFIG_DIR="/home/hm-user/.config/claude"'
'';
}

View File

@@ -1,5 +1,6 @@
{
claude-code-basic = ./basic.nix;
claude-code-config-dir = ./config-dir.nix;
claude-code-full-config = ./full-config.nix;
claude-code-lsp = ./lsp.nix;
claude-code-mcp = ./mcp.nix;