Skip to content

Commit b861254

Browse files
committed
Add configurable CPU and memory for VMs
1 parent 889fe68 commit b861254

4 files changed

Lines changed: 121 additions & 3 deletions

File tree

README.md

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,103 @@
11
# clave
22

3-
Install with:
3+
Spin up ephemeral VMs via [Tart](https://tart.run/) for isolated [Claude Code](https://claude.ai/code) sessions. Run `clave` from within a project's
4+
git repo, and it handles repo cloning, VM lifecycle, networking, and teardown automatically.
5+
6+
## Installation
47

58
```shell
69
curl -fsSL https://clave.run | sh
710
```
11+
12+
## Requirements
13+
14+
- macOS with [Tart](https://tart.run/) installed
15+
- A [Claude Code](https://claude.ai/code) account
16+
17+
## Usage
18+
19+
### Working Directory Mode
20+
21+
Run `clave` from within a project's git repository:
22+
23+
```shell
24+
cd /path/to/your/project
25+
clave
26+
```
27+
28+
Clave will spin up a fresh VM, mount your current working directory into the VM, and launch an interactive Claude Code session
29+
inside it. When you exit, the VM and clone are torn down automatically. This lets you work on the local project similar to running
30+
`claude`, but without having to worry about the agent accidentally deleting your home directory or getting tricked into shipping
31+
your SSH private key somewhere.
32+
33+
### Isolate Mode
34+
35+
Run `clave --isolate` from within a project's git repository:
36+
37+
```shell
38+
cd /path/to/your/project
39+
clave --isolate
40+
```
41+
42+
Before spinning up a fresh VM, clave will create a local git clone of your working directory. (This is similarly efficient as a
43+
worktree, but has a copy of your full git history.) Clave will then mount that clone into the VM, keeping any changes made inside
44+
the VM completely isolated from your project until you quit. Once you quit, you will be prompted to decide what to do with the
45+
changes before the clone is cleaned up.
46+
47+
## Configuration
48+
49+
Add a `.clave.json` file to your project root to customize VM setup for your project.
50+
51+
### `.clave.json`
52+
53+
```json
54+
{
55+
"base_image": "ghcr.io/cirruslabs/ubuntu:latest",
56+
"cpus": 8,
57+
"memory": 16384,
58+
"provision": [
59+
"sudo apt-get install -y redis-server",
60+
"sudo systemctl enable redis-server"
61+
],
62+
"env": [
63+
"STRIPE_SECRET_KEY",
64+
"CUSTOM_API_TOKEN"
65+
]
66+
}
67+
```
68+
69+
### Options
70+
71+
| Key | Type | Description |
72+
|--------------|----------|---------------------------------------------------------------------------|
73+
| `base_image` | string | Custom Tart VM base image. Defaults to `ghcr.io/cirruslabs/ubuntu:latest` |
74+
| `cpus` | integer | CPUs allocated to the VM. Defaults to `CLAVE_VM_CPUS` (4) |
75+
| `memory` | integer | Memory (MB) allocated to the VM. Defaults to `CLAVE_VM_MEMORY` (8192) |
76+
| `provision` | string[] | Bash commands to run during VM provisioning |
77+
| `env` | string[] | Environment variable names to pass through from your host into the VM |
78+
79+
See the [`tart` documentation](https://tart.run/quick-start/#vm-images) for a list of available base images.
80+
81+
### Environment Variables Passed Through Automatically
82+
83+
Clave always forwards these environment variables into the VM when present on the host:
84+
85+
- `COLORTERM`, `FORCE_COLOR`, `NO_COLOR`
86+
- `GIT_AUTHOR_EMAIL`, `GIT_AUTHOR_NAME`, `GIT_COMMITTER_EMAIL`, `GIT_COMMITTER_NAME`
87+
- `LANG`, `LC_ALL`, `LC_CTYPE`
88+
- `TZ`, `VISUAL`
89+
90+
Any additional variables listed in `.clave.json`'s `env` array are forwarded as well.
91+
92+
## Global Configuration
93+
94+
Clave reads the following environment variables for global defaults:
95+
96+
| Variable | Default | Description |
97+
|---------------------------|------------------------------------|--------------------------------------------------|
98+
| `ANTHROPIC_API_KEY` || Anthropic API key for Claude Code |
99+
| `CLAUDE_CODE_OAUTH_TOKEN` || Claude Code OAuth token (alternative to API key) |
100+
| `CLAVE_BASE_IMAGE` | `ghcr.io/cirruslabs/ubuntu:latest` | Default VM base image |
101+
| `CLAVE_BASE_VM` | `clave-base` | Name of the base Tart VM |
102+
| `CLAVE_VM_CPUS` | `4` | CPUs allocated to each VM |
103+
| `CLAVE_VM_MEMORY` | `8192` | Memory (MB) allocated to each VM |

app/Data/ProjectConfig.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,17 @@ public static function fromProjectDir(string $project_dir, Filesystem $fs): stat
2525
base_image: $data['base_image'] ?? null,
2626
provision: $data['provision'] ?? [],
2727
env: $data['env'] ?? [],
28+
cpus: isset($data['cpus']) ? (int) $data['cpus'] : null,
29+
memory: isset($data['memory']) ? (int) $data['memory'] : null,
2830
);
2931
}
3032

3133
public function __construct(
3234
public readonly ?string $base_image = null,
3335
public readonly array $provision = [],
3436
public readonly array $env = [],
37+
public readonly ?int $cpus = null,
38+
public readonly ?int $memory = null,
3539
) {
3640
}
3741

app/Pipelines/Steps/CloneVm.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ public function handle(SessionContext $context, Closure $next): mixed
2525

2626
$context->vm_name = $vm_name;
2727

28-
$cpus = config('clave.vm.cpus');
29-
$memory = config('clave.vm.memory');
28+
$cpus = $context->project_config->cpus ?? config('clave.vm.cpus');
29+
$memory = $context->project_config->memory ?? config('clave.vm.memory');
3030
$display = config('clave.vm.display');
3131

3232
if ($cpus || $memory || $display) {

tests/Unit/Dto/ProjectConfigTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,33 @@
7272
$fs->shouldReceive('get')->with('/path/to/project/.clave.json')->andReturn(json_encode([
7373
'base_image' => 'ghcr.io/custom/image:latest',
7474
'provision' => ['sudo apt-get install -y redis-server'],
75+
'cpus' => 8,
76+
'memory' => 16384,
7577
]));
7678

7779
$config = ProjectConfig::fromProjectDir('/path/to/project', $fs);
7880

7981
expect($config->base_image)->toBe('ghcr.io/custom/image:latest')
8082
->and($config->provision)->toBe(['sudo apt-get install -y redis-server'])
83+
->and($config->cpus)->toBe(8)
84+
->and($config->memory)->toBe(16384)
8185
->and($config->hasCustomizations())->toBeTrue();
8286
});
8387

88+
test('fromProjectDir parses cpus and memory', function() {
89+
$fs = Mockery::mock(Filesystem::class);
90+
$fs->shouldReceive('exists')->with('/path/to/project/.clave.json')->andReturn(true);
91+
$fs->shouldReceive('get')->with('/path/to/project/.clave.json')->andReturn(json_encode([
92+
'cpus' => 2,
93+
'memory' => 4096,
94+
]));
95+
96+
$config = ProjectConfig::fromProjectDir('/path/to/project', $fs);
97+
98+
expect($config->cpus)->toBe(2)
99+
->and($config->memory)->toBe(4096);
100+
});
101+
84102
test('fromProjectDir handles invalid json gracefully', function() {
85103
$fs = Mockery::mock(Filesystem::class);
86104
$fs->shouldReceive('exists')->with('/path/to/project/.clave.json')->andReturn(true);

0 commit comments

Comments
 (0)