Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
33 changes: 33 additions & 0 deletions pkg/config/envkeys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 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"
)
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
@@ -1,13 +1,18 @@
package openclaw

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

import (

Check failure on line 8 in pkg/migrate/sources/openclaw/openclaw_handler.go

View workflow job for this annotation

GitHub Actions / Linter

syntax error: imports must appear before other declarations)) (typecheck)

Check failure on line 8 in pkg/migrate/sources/openclaw/openclaw_handler.go

View workflow job for this annotation

GitHub Actions / Security Check

imports must appear before other declarations

Check failure on line 8 in pkg/migrate/sources/openclaw/openclaw_handler.go

View workflow job for this annotation

GitHub Actions / Tests

syntax error: imports must appear before other declarations
"fmt"
"os"
"path/filepath"
"strings"

"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/migrate/internal"

Check failure on line 15 in pkg/migrate/sources/openclaw/openclaw_handler.go

View workflow job for this annotation

GitHub Actions / Security Check

could not import github.com/sipeed/picoclaw/pkg/migrate/internal (no metadata for github.com/sipeed/picoclaw/pkg/migrate/internal)
)

var providerMapping = map[string]string{
Expand Down Expand Up @@ -112,7 +117,7 @@
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
@@ -1,10 +1,15 @@
package providers

// 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"

import (

Check failure on line 8 in pkg/providers/codex_cli_credentials.go

View workflow job for this annotation

GitHub Actions / Security Check

imports must appear before other declarations

Check failure on line 8 in pkg/providers/codex_cli_credentials.go

View workflow job for this annotation

GitHub Actions / Tests

syntax error: imports must appear before other declarations
"encoding/json"
"fmt"
"os"

Check failure on line 11 in pkg/providers/codex_cli_credentials.go

View workflow job for this annotation

GitHub Actions / Security Check

could not import os (no metadata for os)
"path/filepath"

Check failure on line 12 in pkg/providers/codex_cli_credentials.go

View workflow job for this annotation

GitHub Actions / Security Check

could not import path/filepath (no metadata for path/filepath)
"time"
)

Expand Down Expand Up @@ -69,7 +74,7 @@
}

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/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