1
0
Fork 0
mirror of https://github.com/nix-community/home-manager.git synced 2025-11-08 19:46:05 +01:00
home-manager/modules/programs/claude-code.nix

382 lines
11 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
cfg = config.programs.claude-code;
jsonFormat = pkgs.formats.json { };
in
{
meta.maintainers = [ lib.maintainers.khaneliman ];
options.programs.claude-code = {
enable = lib.mkEnableOption "Claude Code, Anthropic's official CLI";
package = lib.mkPackageOption pkgs "claude-code" { nullable = true; };
finalPackage = lib.mkOption {
type = lib.types.package;
readOnly = true;
internal = true;
description = "Resulting customized claude-code package.";
};
settings = lib.mkOption {
inherit (jsonFormat) type;
default = { };
example = {
theme = "dark";
permissions = {
allow = [
"Bash(git diff:*)"
"Edit"
];
ask = [ "Bash(git push:*)" ];
deny = [
"WebFetch"
"Bash(curl:*)"
"Read(./.env)"
"Read(./secrets/**)"
];
additionalDirectories = [ "../docs/" ];
defaultMode = "acceptEdits";
disableBypassPermissionsMode = "disable";
};
model = "claude-3-5-sonnet-20241022";
hooks = {
PreToolUse = [
{
matcher = "Bash";
hooks = [
{
type = "command";
command = "echo 'Running command: $CLAUDE_TOOL_INPUT'";
}
];
}
];
PostToolUse = [
{
matcher = "Edit|MultiEdit|Write";
hooks = [
{
type = "command";
command = "nix fmt $(jq -r '.tool_input.file_path' <<< '$CLAUDE_TOOL_INPUT')";
}
];
}
];
};
statusLine = {
type = "command";
command = "input=$(cat); echo \"[$(echo \"$input\" | jq -r '.model.display_name')] 📁 $(basename \"$(echo \"$input\" | jq -r '.workspace.current_dir')\")\"";
padding = 0;
};
includeCoAuthoredBy = false;
};
description = "JSON configuration for Claude Code settings.json";
};
agents = lib.mkOption {
type = lib.types.attrsOf lib.types.lines;
default = { };
description = ''
Custom agents for Claude Code.
The attribute name becomes the agent filename, and the value is the file content with frontmatter.
Agents are stored in .claude/agents/ directory.
'';
example = {
code-reviewer = ''
---
name: code-reviewer
description: Specialized code review agent
tools: Read, Edit, Grep
---
You are a senior software engineer specializing in code reviews.
Focus on code quality, security, and maintainability.
'';
documentation = ''
---
name: documentation
description: Documentation writing assistant
model: claude-3-5-sonnet-20241022
tools: Read, Write, Edit
---
You are a technical writer who creates clear, comprehensive documentation.
Focus on user-friendly explanations and examples.
'';
};
};
commands = lib.mkOption {
type = lib.types.attrsOf lib.types.lines;
default = { };
description = ''
Custom commands for Claude Code.
The attribute name becomes the command filename, and the value is the file content.
Commands are stored in .claude/commands/ directory.
'';
example = {
changelog = ''
---
allowed-tools: Bash(git log:*), Bash(git diff:*)
argument-hint: [version] [change-type] [message]
description: Update CHANGELOG.md with new entry
---
Parse the version, change type, and message from the input
and update the CHANGELOG.md file accordingly.
'';
fix-issue = ''
---
allowed-tools: Bash(git status:*), Read
argument-hint: [issue-number]
description: Fix GitHub issue following coding standards
---
Fix issue #$ARGUMENTS following our coding standards and best practices.
'';
commit = ''
---
allowed-tools: Bash(git add:*), Bash(git status:*), Bash(git commit:*)
description: Create a git commit with proper message
---
## Context
- Current git status: !`git status`
- Current git diff: !`git diff HEAD`
- Recent commits: !`git log --oneline -5`
## Task
Based on the changes above, create a single atomic git commit with a descriptive message.
'';
};
};
hooks = lib.mkOption {
type = lib.types.attrsOf lib.types.lines;
default = { };
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.
'';
example = {
pre-edit = ''
#!/usr/bin/env bash
echo "About to edit file: $1"
'';
post-commit = ''
#!/usr/bin/env bash
echo "Committed with message: $1"
'';
};
};
memory = {
text = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = null;
description = ''
Inline memory content for CLAUDE.md.
This option is mutually exclusive with memory.source.
'';
example = ''
# Project Memory
## Current Task
Implementing enhanced claude-code module for home-manager.
## Key Files
- claude-code.nix: Main module implementation
'';
};
source = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Path to a file containing memory content for CLAUDE.md.
This option is mutually exclusive with memory.text.
'';
example = lib.literalExpression "./claude-memory.md";
};
};
agentsDir = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Path to a directory containing agent files for Claude Code.
Agent files from this directory will be symlinked to .claude/agents/.
'';
example = lib.literalExpression "./agents";
};
commandsDir = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Path to a directory containing command files for Claude Code.
Command files from this directory will be symlinked to .claude/commands/.
'';
example = lib.literalExpression "./commands";
};
hooksDir = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Path to a directory containing hook files for Claude Code.
Hook files from this directory will be symlinked to .claude/hooks/.
'';
example = lib.literalExpression "./hooks";
};
mcpServers = lib.mkOption {
type = lib.types.attrsOf jsonFormat.type;
default = { };
description = "MCP (Model Context Protocol) servers configuration";
example = {
github = {
type = "http";
url = "https://api.githubcopilot.com/mcp/";
};
filesystem = {
type = "stdio";
command = "npx";
args = [
"-y"
"@modelcontextprotocol/server-filesystem"
"/tmp"
];
};
database = {
type = "stdio";
command = "npx";
args = [
"-y"
"@bytebase/dbhub"
"--dsn"
"postgresql://user:pass@localhost:5432/db"
];
env = {
DATABASE_URL = "postgresql://user:pass@localhost:5432/db";
};
};
customTransport = {
type = "websocket";
url = "wss://example.com/mcp";
customOption = "value";
timeout = 5000;
};
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.mcpServers == { } || cfg.package != null;
message = "`programs.claude-code.package` cannot be null when `mcpServers` is configured";
}
{
assertion = !(cfg.memory.text != null && cfg.memory.source != null);
message = "Cannot specify both `programs.claude-code.memory.text` and `programs.claude-code.memory.source`";
}
{
assertion = !(cfg.agents != { } && cfg.agentsDir != null);
message = "Cannot specify both `programs.claude-code.agents` and `programs.claude-code.agentsDir`";
}
{
assertion = !(cfg.commands != { } && cfg.commandsDir != null);
message = "Cannot specify both `programs.claude-code.commands` and `programs.claude-code.commandsDir`";
}
{
assertion = !(cfg.hooks != { } && cfg.hooksDir != null);
message = "Cannot specify both `programs.claude-code.hooks` and `programs.claude-code.hooksDir`";
}
];
programs.claude-code.finalPackage =
let
makeWrapperArgs = lib.flatten (
lib.filter (x: x != [ ]) [
(lib.optional (cfg.mcpServers != { }) [
"--add-flags"
"--mcp-config ${jsonFormat.generate "claude-code-mcp-config.json" { inherit (cfg) mcpServers; }}"
])
]
);
hasWrapperArgs = makeWrapperArgs != [ ];
in
if hasWrapperArgs then
pkgs.symlinkJoin {
name = "claude-code";
paths = [ cfg.package ];
nativeBuildInputs = [ pkgs.makeWrapper ];
postBuild = ''
wrapProgram $out/bin/claude ${lib.escapeShellArgs makeWrapperArgs}
'';
inherit (cfg.package) meta;
}
else
cfg.package;
home = {
packages = lib.mkIf (cfg.package != null) [ cfg.finalPackage ];
file = {
".claude/settings.json" = lib.mkIf (cfg.settings != { }) {
source = jsonFormat.generate "claude-code-settings.json" (
cfg.settings
// {
"$schema" = "https://json.schemastore.org/claude-code-settings.json";
}
);
};
".claude/CLAUDE.md" = lib.mkIf (cfg.memory.text != null || cfg.memory.source != null) (
if cfg.memory.text != null then { text = cfg.memory.text; } else { source = cfg.memory.source; }
);
".claude/agents" = lib.mkIf (cfg.agentsDir != null) {
source = cfg.agentsDir;
recursive = true;
};
".claude/commands" = lib.mkIf (cfg.commandsDir != null) {
source = cfg.commandsDir;
recursive = true;
};
".claude/hooks" = lib.mkIf (cfg.hooksDir != null) {
source = cfg.hooksDir;
recursive = true;
};
}
// lib.mapAttrs' (
name: content:
lib.nameValuePair ".claude/agents/${name}.md" {
text = content;
}
) cfg.agents
// lib.mapAttrs' (
name: content:
lib.nameValuePair ".claude/commands/${name}.md" {
text = content;
}
) cfg.commands
// lib.mapAttrs' (
name: content:
lib.nameValuePair ".claude/hooks/${name}" {
text = content;
}
) cfg.hooks;
};
};
}