Skip to content
Merged
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
4 changes: 2 additions & 2 deletions cmd/picoclaw/internal/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ const Logo = "🦞"
// GetPicoclawHome returns the picoclaw home directory.
// Priority: $PICOCLAW_HOME > ~/.picoclaw
func GetPicoclawHome() string {
if home := os.Getenv("PICOCLAW_HOME"); home != "" {
if home := os.Getenv(config.EnvHome); home != "" {
return home
}
home, _ := os.UserHomeDir()
return filepath.Join(home, ".picoclaw")
}

func GetConfigPath() string {
if configPath := os.Getenv("PICOCLAW_CONFIG"); configPath != "" {
if configPath := os.Getenv(config.EnvConfig); configPath != "" {
return configPath
}
return filepath.Join(GetPicoclawHome(), "config.json")
Expand Down
4 changes: 2 additions & 2 deletions pkg/agent/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (cb *ContextBuilder) WithToolDiscovery(useBM25, useRegex bool) *ContextBuil
}

func getGlobalConfigDir() string {
if home := os.Getenv("PICOCLAW_HOME"); home != "" {
if home := os.Getenv(config.EnvHome); home != "" {
return home
}
home, err := os.UserHomeDir()
Expand All @@ -65,7 +65,7 @@ func getGlobalConfigDir() string {
func NewContextBuilder(workspace string) *ContextBuilder {
// builtin skills: skills directory in current project
// Use the skills/ directory under the current working directory
builtinSkillsDir := strings.TrimSpace(os.Getenv("PICOCLAW_BUILTIN_SKILLS"))
builtinSkillsDir := strings.TrimSpace(os.Getenv(config.EnvBuiltinSkills))
if builtinSkillsDir == "" {
wd, _ := os.Getwd()
builtinSkillsDir = filepath.Join(wd, "skills")
Expand Down
3 changes: 2 additions & 1 deletion pkg/auth/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"path/filepath"
"time"

"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/fileutil"
)

Expand Down Expand Up @@ -39,7 +40,7 @@ func (c *AuthCredential) NeedsRefresh() bool {
}

func authFilePath() string {
if home := os.Getenv("PICOCLAW_HOME"); home != "" {
if home := os.Getenv(config.EnvHome); home != "" {
return filepath.Join(home, "auth.json")
}
home, _ := os.UserHomeDir()
Expand Down
2 changes: 1 addition & 1 deletion pkg/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func DefaultConfig() *Config {
// Determine the base path for the workspace.
// Priority: $PICOCLAW_HOME > ~/.picoclaw
var homePath string
if picoclawHome := os.Getenv("PICOCLAW_HOME"); picoclawHome != "" {
if picoclawHome := os.Getenv(EnvHome); picoclawHome != "" {
homePath = picoclawHome
} else {
userHome, _ := os.UserHomeDir()
Expand Down
37 changes: 37 additions & 0 deletions pkg/config/envkeys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
//
// Copyright (c) 2026 PicoClaw contributors

package config

// Runtime environment variable keys for the picoclaw process.
// These control the location of files and binaries at runtime and are read
// directly via os.Getenv / os.LookupEnv. All picoclaw-specific keys use the
// PICOCLAW_ prefix. Reference these constants instead of inline string
// literals to keep all supported knobs visible in one place and to prevent
// typos.
const (
// EnvHome overrides the base directory for all picoclaw data
// (config, workspace, skills, auth store, …).
// Default: ~/.picoclaw
EnvHome = "PICOCLAW_HOME"

// EnvConfig overrides the full path to the JSON config file.
// Default: $PICOCLAW_HOME/config.json
EnvConfig = "PICOCLAW_CONFIG"

// EnvBuiltinSkills overrides the directory from which built-in
// skills are loaded.
// Default: <cwd>/skills
EnvBuiltinSkills = "PICOCLAW_BUILTIN_SKILLS"

// EnvBinary overrides the path to the picoclaw executable.
// Used by the web launcher when spawning the gateway subprocess.
// Default: resolved from the same directory as the current executable.
EnvBinary = "PICOCLAW_BINARY"

// EnvGatewayHost overrides the host address for the gateway server.
// Default: "127.0.0.1"
EnvGatewayHost = "PICOCLAW_GATEWAY_HOST"
)
15 changes: 11 additions & 4 deletions pkg/credential/credential.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,21 @@ var ErrPassphraseRequired = errors.New("credential: enc:// passphrase required")
// indicating a wrong passphrase or SSH key. Callers can detect this with errors.Is.
var ErrDecryptionFailed = errors.New("credential: enc:// decryption failed (wrong passphrase or SSH key?)")

// SSHKeyPathEnvVar is the environment variable that specifies the path to the
// SSH private key used for enc:// credential encryption and decryption.
const SSHKeyPathEnvVar = "PICOCLAW_SSH_KEY_PATH"

// picoclawHome is a package-local copy of config.EnvHome. It is kept here to
// avoid a circular import between pkg/credential and pkg/config.
const picoclawHome = "PICOCLAW_HOME"

const (
fileScheme = "file://"
encScheme = "enc://"
hkdfInfo = "picoclaw-credential-v1"
saltLen = 16
nonceLen = 12
keyLen = 32
sshKeyEnv = "PICOCLAW_SSH_KEY_PATH"
)

// Resolver resolves raw credential strings for model_list api_key fields.
Expand Down Expand Up @@ -248,14 +255,14 @@ func allowedSSHKeyPath(path string) bool {
clean := filepath.Clean(path)

// Exact match with PICOCLAW_SSH_KEY_PATH.
if envPath, ok := os.LookupEnv(sshKeyEnv); ok && envPath != "" {
if envPath, ok := os.LookupEnv(SSHKeyPathEnvVar); ok && envPath != "" {
if clean == filepath.Clean(envPath) {
return true
}
}

// Within PICOCLAW_HOME.
if picoHome := os.Getenv("PICOCLAW_HOME"); picoHome != "" {
if picoHome := os.Getenv(picoclawHome); picoHome != "" {
if isWithinDir(clean, picoHome) {
return true
}
Expand Down Expand Up @@ -316,7 +323,7 @@ func pickSSHKeyPath(override string) string {
if override != "" {
return override
}
if p, ok := os.LookupEnv(sshKeyEnv); ok {
if p, ok := os.LookupEnv(SSHKeyPathEnvVar); ok {
return p // respect explicit setting, even if ""
}
return findDefaultSSHKey()
Expand Down
4 changes: 3 additions & 1 deletion pkg/migrate/internal/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import (
"io"
"os"
"path/filepath"

"github.com/sipeed/picoclaw/pkg/config"
)

func ResolveTargetHome(override string) (string, error) {
if override != "" {
return ExpandHome(override), nil
}
if envHome := os.Getenv("PICOCLAW_HOME"); envHome != "" {
if envHome := os.Getenv(config.EnvHome); envHome != "" {
return ExpandHome(envHome), nil
}
home, err := os.UserHomeDir()
Expand Down
7 changes: 6 additions & 1 deletion pkg/migrate/sources/openclaw/openclaw_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import (
"github.com/sipeed/picoclaw/pkg/migrate/internal"
)

// OpenclawHomeEnvVar is the environment variable that overrides the source
// openclaw home directory when migrating from openclaw to picoclaw.
// Default: ~/.openclaw
const OpenclawHomeEnvVar = "OPENCLAW_HOME"

var providerMapping = map[string]string{
"anthropic": "anthropic",
"claude": "anthropic",
Expand Down Expand Up @@ -112,7 +117,7 @@ func resolveSourceHome(override string) (string, error) {
if override != "" {
return internal.ExpandHome(override), nil
}
if envHome := os.Getenv("OPENCLAW_HOME"); envHome != "" {
if envHome := os.Getenv(OpenclawHomeEnvVar); envHome != "" {
return internal.ExpandHome(envHome), nil
}
home, err := os.UserHomeDir()
Expand Down
7 changes: 6 additions & 1 deletion pkg/providers/codex_cli_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import (
"time"
)

// CodexHomeEnvVar is the environment variable that overrides the Codex CLI
// home directory when resolving the codex auth.json credentials file.
// Default: ~/.codex
const CodexHomeEnvVar = "CODEX_HOME"

// CodexCliAuth represents the ~/.codex/auth.json file structure.
type CodexCliAuth struct {
Tokens struct {
Expand Down Expand Up @@ -69,7 +74,7 @@ func CreateCodexCliTokenSource() func() (string, string, error) {
}

func resolveCodexAuthPath() (string, error) {
codexHome := os.Getenv("CODEX_HOME")
codexHome := os.Getenv(CodexHomeEnvVar)
if codexHome == "" {
home, err := os.UserHomeDir()
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions web/backend/api/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,10 +387,10 @@ func (h *Handler) startGatewayLocked(initialStatus string, existingPid int) (int
// GetConfigPath() already reads, so the gateway sub-process uses the same
// config file without requiring a --config flag on the gateway subcommand.
if h.configPath != "" {
cmd.Env = append(cmd.Env, "PICOCLAW_CONFIG="+h.configPath)
cmd.Env = append(cmd.Env, config.EnvConfig+"="+h.configPath)
}
if host := h.gatewayHostOverride(); host != "" {
cmd.Env = append(cmd.Env, "PICOCLAW_GATEWAY_HOST="+host)
cmd.Env = append(cmd.Env, config.EnvGatewayHost+"="+host)
}

stdoutPipe, err := cmd.StdoutPipe()
Expand Down
4 changes: 2 additions & 2 deletions web/backend/api/skills.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ func loadSkillContent(path string) (string, error) {
}

func globalConfigDir() string {
if home := os.Getenv("PICOCLAW_HOME"); home != "" {
if home := os.Getenv(config.EnvHome); home != "" {
return home
}
home, err := os.UserHomeDir()
Expand All @@ -320,7 +320,7 @@ func globalConfigDir() string {
}

func builtinSkillsDir() string {
if path := os.Getenv("PICOCLAW_BUILTIN_SKILLS"); path != "" {
if path := os.Getenv(config.EnvBuiltinSkills); path != "" {
return path
}
wd, err := os.Getwd()
Expand Down
4 changes: 3 additions & 1 deletion web/backend/utils/onboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"os"
"os/exec"
"strings"

"github.com/sipeed/picoclaw/pkg/config"
)

var execCommand = exec.Command
Expand All @@ -19,7 +21,7 @@ func EnsureOnboarded(configPath string) error {
}

cmd := execCommand(FindPicoclawBinary(), "onboard")
cmd.Env = append(os.Environ(), "PICOCLAW_CONFIG="+configPath)
cmd.Env = append(os.Environ(), config.EnvConfig+"="+configPath)
cmd.Stdin = strings.NewReader("n\n")

output, err := cmd.CombinedOutput()
Expand Down
9 changes: 6 additions & 3 deletions web/backend/utils/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,23 @@ import (
"os/exec"
"path/filepath"
"runtime"

"github.com/sipeed/picoclaw/pkg/config"
)

// GetPicoclawHome returns the picoclaw home directory.
// Priority: $PICOCLAW_HOME > ~/.picoclaw
func GetPicoclawHome() string {
if home := os.Getenv("PICOCLAW_HOME"); home != "" {
if home := os.Getenv(config.EnvHome); home != "" {
return home
}
home, _ := os.UserHomeDir()
return filepath.Join(home, ".picoclaw")
}

// GetDefaultConfigPath returns the default path to the picoclaw config file.
func GetDefaultConfigPath() string {
if configPath := os.Getenv("PICOCLAW_CONFIG"); configPath != "" {
if configPath := os.Getenv(config.EnvConfig); configPath != "" {
return configPath
}
return filepath.Join(GetPicoclawHome(), "config.json")
Expand All @@ -37,7 +40,7 @@ func FindPicoclawBinary() string {
binaryName = "picoclaw.exe"
}

if p := os.Getenv("PICOCLAW_BINARY"); p != "" {
if p := os.Getenv(config.EnvBinary); p != "" {
if info, _ := os.Stat(p); info != nil && !info.IsDir() {
return p
}
Expand Down
Loading