Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion lib/resolve.nix
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ let

# --- Plugin resolution ---

resolvePlugin =
# Import plugin source by path or name string
importPlugin =
ref:
if builtins.isPath ref then
import ref
Expand All @@ -57,6 +58,24 @@ let
else
import (pluginsDir + "/${ref}");

# Call a plugin function with args, or return plain attrset as-is
callPlugin = imported: args: if builtins.isFunction imported then imported args else imported;

# Resolve a plugin reference:
# "claude-code" → import + call with {}
# { plugin = "claude-code"; configDir = "~/.x"; } → import + call with { configDir = "~/.x"; }
# /path/to/plugin.nix → import + call with {}
resolvePlugin =
ref:
if builtins.isAttrs ref then
let
imported = importPlugin (ref.plugin or (throw "plugin attr requires a 'plugin' key"));
args = builtins.removeAttrs ref [ "plugin" ];
in
callPlugin imported args
else
callPlugin (importPlugin ref) { };

pluginConfigs = map resolvePlugin (config.plugins or [ ]);

# Strip meta fields before merging
Expand Down
12 changes: 11 additions & 1 deletion plugins/claude-code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,25 @@ Runs [Claude Code](https://docs.anthropic.com/en/docs/claude-code) inside the VM
## Usage

```nix
# Default — uses ~/.claude on the host
{ plugins = [ "claude-code" ]; }

# Custom config directory — use a separate Claude installation
{ plugins = [ { plugin = "claude-code"; configDir = "~/.claude-work"; } ]; }
```

## Parameters

| Name | Default | Description |
|---|---|---|
| `configDir` | `~/.claude` | Host-side directory to mount as `~/.claude` inside the VM |

## What it provides

| Category | Details |
|---|---|
| **Packages** | `claude-code` |
| **Mounts** | `~/.claude` — preserves config, credentials, and session history across VM restarts |
| **Mounts** | `<configDir>` → `~/.claude` — preserves config, credentials, and session history across VM restarts |
| **Domains** | `anthropic.com`, `claude.com`, `sentry.io` |
| **Setup** | Overlays tmpfs on `~/.claude/{debug,statsig,telemetry,todos}` to work around virtiofs lacking `O_TMPFILE` support |

Expand Down
3 changes: 2 additions & 1 deletion plugins/claude-code/default.nix
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{ configDir ? "~/.claude" }:
{
nix.packages = [ "claude-code" ];

mounts = [
{
source = "~/.claude";
source = configDir;
target = "~/.claude";
}
];
Expand Down
10 changes: 10 additions & 0 deletions tests/run-nix-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ assert_eq "user domain" "$(jq_get "$json" '.network.domains | index("user.exampl
# Hook from plugin
assert_eq "plugin hook" "$(jq_get "$json" '.hooks."post-up"[0]')" "echo plugin-loaded"

# ---------------------------------------------------------------------------
echo "==> Test: parameterized plugin (function with args)"
json=$(resolve "$FIXTURES/with_plugin_args.nix")

assert_eq "projectName" "$(jq_get "$json" '.projectName')" "test-plugin-args"
assert_eq "plugin env passed through" "$(jq_get "$json" '.env.GREETING')" "world"
assert_eq "plugin packages" "$(jq_get "$json" '.nix.packages | index("curl") != null')" "true"
assert_eq "plugin domain" "$(jq_get "$json" '.network.domains | index("fn.example.com") != null')" "true"
assert_eq "user domain" "$(jq_get "$json" '.network.domains | index("user.example.com") != null')" "true"

# ---------------------------------------------------------------------------
echo "==> Test: empty mounts get default"
json=$(resolve "$FIXTURES/empty_mounts.nix")
Expand Down
Loading