diff --git a/README.md b/README.md index d963a010..ad47b5e7 100644 --- a/README.md +++ b/README.md @@ -348,3 +348,40 @@ OPENROUTER_API_KEY=sk-or-v1-xxxxx \ - `OPENROUTER_API_KEY` - Skip OAuth and use this API key directly - `LINODE_TYPE` - Instance type (default: `g6-standard-1`) - `LINODE_REGION` - Datacenter region (default: `us-east`) + +--- + +## Security + +### API Token Storage + +Spawn stores cloud provider API tokens and OpenRouter API keys locally in JSON files at `~/.config/spawn/`: + +- `hetzner.json` - Hetzner Cloud API token +- `digitalocean.json` - DigitalOcean API token +- `vultr.json` - Vultr API key +- `linode.json` - Linode API token +- OpenRouter API keys stored in shell config files (`~/.bashrc`, `~/.zshrc`) + +**Security Posture:** +- All token files are created with `chmod 600` (user read/write only) +- Tokens are stored in **plaintext** - not encrypted at rest +- Security relies on filesystem permissions and OS user isolation + +**Recommendations:** +1. **Protect your user account** - Use strong passwords, disk encryption, and secure your SSH keys +2. **Use dedicated API tokens** - Create tokens specifically for Spawn with minimal required permissions +3. **Rotate tokens regularly** - Revoke and regenerate API tokens periodically +4. **Multi-user systems** - On shared machines, be aware that root users can read these files +5. **Backup security** - Ensure backups of `~/.config/` are encrypted + +**Why plaintext?** +- Simplicity and compatibility across all Unix-like systems +- File permissions (`600`) provide adequate protection for single-user machines +- Encryption at rest would require key management, adding complexity without significant security benefit for typical use cases +- Cloud providers recommend similar approaches for CLI tools (AWS CLI, gcloud, etc.) + +**Alternative approaches:** +- For higher security requirements, consider using environment variables instead of saved tokens +- Pass `OPENROUTER_API_KEY`, `HCLOUD_TOKEN`, etc. as environment variables on each run +- Use OS credential stores (Keychain on macOS, Secret Service on Linux) - requires additional dependencies diff --git a/digitalocean/aider.sh b/digitalocean/aider.sh index b72dabdb..9c70d025 100755 --- a/digitalocean/aider.sh +++ b/digitalocean/aider.sh @@ -51,6 +51,7 @@ if ! validate_model_id "$MODEL_ID"; then log_error "Exiting due to invalid model log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/digitalocean/claude.sh b/digitalocean/claude.sh index 027b87e0..93e5366a 100755 --- a/digitalocean/claude.sh +++ b/digitalocean/claude.sh @@ -46,6 +46,7 @@ fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] @@ -67,6 +68,7 @@ log_warn "Configuring Claude Code..." run_server "$DO_SERVER_IP" "mkdir -p ~/.claude" SETTINGS_TEMP=$(mktemp) +chmod 600 "$SETTINGS_TEMP" cat > "$SETTINGS_TEMP" << EOF { "theme": "dark", @@ -87,6 +89,7 @@ upload_file "$DO_SERVER_IP" "$SETTINGS_TEMP" "/root/.claude/settings.json" rm "$SETTINGS_TEMP" GLOBAL_STATE_TEMP=$(mktemp) +chmod 600 "$GLOBAL_STATE_TEMP" cat > "$GLOBAL_STATE_TEMP" << EOF { "hasCompletedOnboarding": true, diff --git a/digitalocean/codex.sh b/digitalocean/codex.sh index bd3890bd..2046a8a1 100755 --- a/digitalocean/codex.sh +++ b/digitalocean/codex.sh @@ -32,6 +32,7 @@ fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/digitalocean/goose.sh b/digitalocean/goose.sh index 964f6138..8bcbb4ed 100755 --- a/digitalocean/goose.sh +++ b/digitalocean/goose.sh @@ -43,6 +43,7 @@ fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/digitalocean/interpreter.sh b/digitalocean/interpreter.sh index 17059afc..8ff7f155 100755 --- a/digitalocean/interpreter.sh +++ b/digitalocean/interpreter.sh @@ -32,6 +32,7 @@ fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/digitalocean/nanoclaw.sh b/digitalocean/nanoclaw.sh index 88e15972..55a394d8 100755 --- a/digitalocean/nanoclaw.sh +++ b/digitalocean/nanoclaw.sh @@ -46,6 +46,7 @@ fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] @@ -62,6 +63,7 @@ rm "$ENV_TEMP" log_warn "Configuring nanoclaw..." DOTENV_TEMP=$(mktemp) +chmod 600 "$DOTENV_TEMP" cat > "$DOTENV_TEMP" << EOF ANTHROPIC_API_KEY=${OPENROUTER_API_KEY} EOF diff --git a/digitalocean/openclaw.sh b/digitalocean/openclaw.sh index 2bb42078..e0a5ed2b 100755 --- a/digitalocean/openclaw.sh +++ b/digitalocean/openclaw.sh @@ -51,6 +51,7 @@ if ! validate_model_id "$MODEL_ID"; then log_error "Exiting due to invalid model log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/hetzner/aider.sh b/hetzner/aider.sh index 64175f72..c18940d5 100755 --- a/hetzner/aider.sh +++ b/hetzner/aider.sh @@ -51,6 +51,7 @@ if ! validate_model_id "$MODEL_ID"; then log_error "Exiting due to invalid model log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/hetzner/claude.sh b/hetzner/claude.sh index 6996ac61..0cefd668 100755 --- a/hetzner/claude.sh +++ b/hetzner/claude.sh @@ -46,6 +46,7 @@ fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] @@ -68,6 +69,7 @@ run_server "$HETZNER_SERVER_IP" "mkdir -p ~/.claude" # Upload settings.json SETTINGS_TEMP=$(mktemp) +chmod 600 "$SETTINGS_TEMP" cat > "$SETTINGS_TEMP" << EOF { "theme": "dark", @@ -89,6 +91,7 @@ rm "$SETTINGS_TEMP" # Upload ~/.claude.json global state GLOBAL_STATE_TEMP=$(mktemp) +chmod 600 "$GLOBAL_STATE_TEMP" cat > "$GLOBAL_STATE_TEMP" << EOF { "hasCompletedOnboarding": true, diff --git a/hetzner/codex.sh b/hetzner/codex.sh index 1a8f2ff2..489cc295 100755 --- a/hetzner/codex.sh +++ b/hetzner/codex.sh @@ -32,6 +32,7 @@ fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/hetzner/goose.sh b/hetzner/goose.sh index a4b71e12..6f6692d6 100755 --- a/hetzner/goose.sh +++ b/hetzner/goose.sh @@ -43,6 +43,7 @@ fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/hetzner/interpreter.sh b/hetzner/interpreter.sh index ae19a2cd..ececb6d1 100755 --- a/hetzner/interpreter.sh +++ b/hetzner/interpreter.sh @@ -32,6 +32,7 @@ fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/hetzner/nanoclaw.sh b/hetzner/nanoclaw.sh index dfcbe9dc..cf7ebce7 100755 --- a/hetzner/nanoclaw.sh +++ b/hetzner/nanoclaw.sh @@ -46,6 +46,7 @@ fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] @@ -62,6 +63,7 @@ rm "$ENV_TEMP" log_warn "Configuring nanoclaw..." DOTENV_TEMP=$(mktemp) +chmod 600 "$DOTENV_TEMP" cat > "$DOTENV_TEMP" << EOF ANTHROPIC_API_KEY=${OPENROUTER_API_KEY} EOF diff --git a/hetzner/openclaw.sh b/hetzner/openclaw.sh index f8e22929..90c2ee5d 100755 --- a/hetzner/openclaw.sh +++ b/hetzner/openclaw.sh @@ -51,6 +51,7 @@ if ! validate_model_id "$MODEL_ID"; then log_error "Exiting due to invalid model log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/linode/aider.sh b/linode/aider.sh index a57113ea..ba874f33 100755 --- a/linode/aider.sh +++ b/linode/aider.sh @@ -24,6 +24,7 @@ MODEL_ID="${MODEL_ID:-openrouter/auto}" if ! validate_model_id "$MODEL_ID"; then log_error "Exiting due to invalid model ID"; exit 1; fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/linode/claude.sh b/linode/claude.sh index af12a8e7..4af9e459 100755 --- a/linode/claude.sh +++ b/linode/claude.sh @@ -22,6 +22,7 @@ if [[ -n "$OPENROUTER_API_KEY" ]]; then log_info "Using OpenRouter API key from else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180); fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] @@ -38,6 +39,7 @@ rm "$ENV_TEMP" log_warn "Configuring Claude Code..." run_server "$LINODE_SERVER_IP" "mkdir -p ~/.claude" SETTINGS_TEMP=$(mktemp) +chmod 600 "$SETTINGS_TEMP" cat > "$SETTINGS_TEMP" << EOF { "theme": "dark", @@ -56,6 +58,7 @@ EOF upload_file "$LINODE_SERVER_IP" "$SETTINGS_TEMP" "/root/.claude/settings.json" rm "$SETTINGS_TEMP" GLOBAL_STATE_TEMP=$(mktemp) +chmod 600 "$GLOBAL_STATE_TEMP" cat > "$GLOBAL_STATE_TEMP" << EOF { "hasCompletedOnboarding": true, diff --git a/linode/codex.sh b/linode/codex.sh index e3af3e06..29cdf5b4 100755 --- a/linode/codex.sh +++ b/linode/codex.sh @@ -19,6 +19,7 @@ if [[ -n "$OPENROUTER_API_KEY" ]]; then log_info "Using OpenRouter API key from else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180); fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/linode/goose.sh b/linode/goose.sh index 18a3b5f8..c50ac277 100755 --- a/linode/goose.sh +++ b/linode/goose.sh @@ -19,6 +19,7 @@ if [[ -n "$OPENROUTER_API_KEY" ]]; then log_info "Using OpenRouter API key from else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180); fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/linode/interpreter.sh b/linode/interpreter.sh index 92a08dd9..83f09550 100755 --- a/linode/interpreter.sh +++ b/linode/interpreter.sh @@ -19,6 +19,7 @@ if [[ -n "$OPENROUTER_API_KEY" ]]; then log_info "Using OpenRouter API key from else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180); fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/linode/nanoclaw.sh b/linode/nanoclaw.sh index 81ac923e..6ac90df7 100755 --- a/linode/nanoclaw.sh +++ b/linode/nanoclaw.sh @@ -21,6 +21,7 @@ if [[ -n "$OPENROUTER_API_KEY" ]]; then log_info "Using OpenRouter API key from else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180); fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] @@ -33,6 +34,7 @@ run_server "$LINODE_SERVER_IP" "cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_c rm "$ENV_TEMP" log_warn "Configuring nanoclaw..." DOTENV_TEMP=$(mktemp) +chmod 600 "$DOTENV_TEMP" cat > "$DOTENV_TEMP" << EOF ANTHROPIC_API_KEY=${OPENROUTER_API_KEY} EOF diff --git a/linode/openclaw.sh b/linode/openclaw.sh index 99693ba9..c92b683e 100755 --- a/linode/openclaw.sh +++ b/linode/openclaw.sh @@ -24,6 +24,7 @@ MODEL_ID="${MODEL_ID:-openrouter/auto}" if ! validate_model_id "$MODEL_ID"; then log_error "Exiting due to invalid model ID"; exit 1; fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/sprite/aider.sh b/sprite/aider.sh index 42eecee3..ac87c15c 100755 --- a/sprite/aider.sh +++ b/sprite/aider.sh @@ -54,6 +54,7 @@ fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/sprite/claude.sh b/sprite/claude.sh index 360190d5..6e5025a3 100755 --- a/sprite/claude.sh +++ b/sprite/claude.sh @@ -38,6 +38,7 @@ log_warn "Setting up environment variables..." # Create temp file with env config ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] @@ -60,6 +61,7 @@ run_sprite "$SPRITE_NAME" "mkdir -p ~/.claude" # Create Claude settings.json via file upload SETTINGS_TEMP=$(mktemp) +chmod 600 "$SETTINGS_TEMP" cat > "$SETTINGS_TEMP" << EOF { "theme": "dark", @@ -81,6 +83,7 @@ rm "$SETTINGS_TEMP" # Create ~/.claude.json global state to skip onboarding and trust dialogs GLOBAL_STATE_TEMP=$(mktemp) +chmod 600 "$GLOBAL_STATE_TEMP" cat > "$GLOBAL_STATE_TEMP" << EOF { "hasCompletedOnboarding": true, diff --git a/sprite/codex.sh b/sprite/codex.sh index 6e45f34a..caf6d952 100755 --- a/sprite/codex.sh +++ b/sprite/codex.sh @@ -33,6 +33,7 @@ fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/sprite/goose.sh b/sprite/goose.sh index 57d35fa4..f5f54a6d 100755 --- a/sprite/goose.sh +++ b/sprite/goose.sh @@ -41,6 +41,7 @@ fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/sprite/interpreter.sh b/sprite/interpreter.sh index 502dba22..45473ad0 100755 --- a/sprite/interpreter.sh +++ b/sprite/interpreter.sh @@ -33,6 +33,7 @@ fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/sprite/nanoclaw.sh b/sprite/nanoclaw.sh index 6c8cce51..f579368a 100644 --- a/sprite/nanoclaw.sh +++ b/sprite/nanoclaw.sh @@ -41,6 +41,7 @@ OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] @@ -56,6 +57,7 @@ rm "$ENV_TEMP" log_warn "Configuring nanoclaw..." DOTENV_TEMP=$(mktemp) +chmod 600 "$DOTENV_TEMP" cat > "$DOTENV_TEMP" << EOF ANTHROPIC_API_KEY=${OPENROUTER_API_KEY} EOF diff --git a/sprite/openclaw.sh b/sprite/openclaw.sh index 37460262..09876bbf 100755 --- a/sprite/openclaw.sh +++ b/sprite/openclaw.sh @@ -50,6 +50,7 @@ log_warn "Setting up environment variables..." # Create temp file with env config ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/vultr/aider.sh b/vultr/aider.sh index f945faae..7a2f2c57 100755 --- a/vultr/aider.sh +++ b/vultr/aider.sh @@ -38,6 +38,7 @@ if ! validate_model_id "$MODEL_ID"; then log_error "Exiting due to invalid model log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/vultr/claude.sh b/vultr/claude.sh index 9ea22e47..5b1622a5 100755 --- a/vultr/claude.sh +++ b/vultr/claude.sh @@ -35,6 +35,7 @@ fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] @@ -53,6 +54,7 @@ log_warn "Configuring Claude Code..." run_server "$VULTR_SERVER_IP" "mkdir -p ~/.claude" SETTINGS_TEMP=$(mktemp) +chmod 600 "$SETTINGS_TEMP" cat > "$SETTINGS_TEMP" << EOF { "theme": "dark", @@ -72,6 +74,7 @@ upload_file "$VULTR_SERVER_IP" "$SETTINGS_TEMP" "/root/.claude/settings.json" rm "$SETTINGS_TEMP" GLOBAL_STATE_TEMP=$(mktemp) +chmod 600 "$GLOBAL_STATE_TEMP" cat > "$GLOBAL_STATE_TEMP" << EOF { "hasCompletedOnboarding": true, diff --git a/vultr/codex.sh b/vultr/codex.sh index 31c58449..fe792d61 100755 --- a/vultr/codex.sh +++ b/vultr/codex.sh @@ -32,6 +32,7 @@ fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/vultr/goose.sh b/vultr/goose.sh index 189ff1ec..7d31e7b6 100755 --- a/vultr/goose.sh +++ b/vultr/goose.sh @@ -32,6 +32,7 @@ fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/vultr/interpreter.sh b/vultr/interpreter.sh index 8b1301db..8dc97710 100755 --- a/vultr/interpreter.sh +++ b/vultr/interpreter.sh @@ -32,6 +32,7 @@ fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] diff --git a/vultr/nanoclaw.sh b/vultr/nanoclaw.sh index a383dd91..94accbf1 100755 --- a/vultr/nanoclaw.sh +++ b/vultr/nanoclaw.sh @@ -34,6 +34,7 @@ fi log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env] @@ -47,6 +48,7 @@ rm "$ENV_TEMP" log_warn "Configuring nanoclaw..." DOTENV_TEMP=$(mktemp) +chmod 600 "$DOTENV_TEMP" cat > "$DOTENV_TEMP" << EOF ANTHROPIC_API_KEY=${OPENROUTER_API_KEY} EOF diff --git a/vultr/openclaw.sh b/vultr/openclaw.sh index 799f72db..4ccf4be8 100755 --- a/vultr/openclaw.sh +++ b/vultr/openclaw.sh @@ -38,6 +38,7 @@ if ! validate_model_id "$MODEL_ID"; then log_error "Exiting due to invalid model log_warn "Setting up environment variables..." ENV_TEMP=$(mktemp) +chmod 600 "$ENV_TEMP" cat > "$ENV_TEMP" << EOF # [spawn:env]