PicoClaw supports separating sensitive data (API keys, tokens, secrets, passwords) from the main configuration by storing them in a .security.yml file. This improves security by:
- Separation of concerns: Configuration settings and secrets are in separate files
- Easier sharing: The main config can be shared without exposing sensitive data
- Better version control:
.security.ymlshould be added to.gitignore - Flexible deployment: Different environments can use different security files
~/.picoclaw/
├── config.json # Main configuration (safe to share)
└── .security.yml # Security data (never share)
The security configuration works through direct field mapping, NOT through ref: string references. The system automatically loads values from .security.yml and applies them to the corresponding fields in config.json.
- Values in
.security.ymlare automatically mapped to corresponding fields in the config - The mapping is based on field names and structure, not on reference strings
- If a value exists in
.security.yml, it overrides the value inconfig.json - You can omit sensitive fields from
config.jsonentirely (recommended)
# Model API Keys
# All models MUST use `api_keys` (plural) array format
# Even a single key must be provided as an array with one element
model_list:
gpt-5.4:
api_keys:
- "sk-proj-your-actual-openai-key-1"
- "sk-proj-your-actual-openai-key-2" # Optional: Multiple keys for failover
claude-sonnet-4.6:
api_keys:
- "sk-ant-your-actual-anthropic-key" # Single key in array format
# Channel Tokens
channels:
telegram:
token: "your-telegram-bot-token"
feishu:
app_secret: "your-feishu-app-secret"
encrypt_key: "your-feishu-encrypt-key"
verification_token: "your-feishu-verification-token"
discord:
token: "your-discord-bot-token"
weixin:
token: "your-weixin-token"
qq:
app_secret: "your-qq-app-secret"
dingtalk:
client_secret: "your-dingtalk-client-secret"
slack:
bot_token: "your-slack-bot-token"
app_token: "your-slack-app-token"
matrix:
access_token: "your-matrix-access-token"
line:
channel_secret: "your-line-channel-secret"
channel_access_token: "your-line-channel-access-token"
onebot:
access_token: "your-onebot-access-token"
wecom:
token: "your-wecom-token"
encoding_aes_key: "your-wecom-encoding-aes-key"
wecom_app:
corp_secret: "your-wecom-app-corp-secret"
token: "your-wecom-app-token"
encoding_aes_key: "your-wecom-app-encoding-aes-key"
wecom_aibot:
secret: "your-wecom-aibot-secret"
token: "your-wecom-aibot-token"
encoding_aes_key: "your-wecom-aibot-encoding-aes-key"
pico:
token: "your-pico-token"
irc:
password: "your-irc-password"
nickserv_password: "your-irc-nickserv-password"
sasl_password: "your-irc-sasl-password"
# Channel Settings (nested format for channels that use settings block)
channel_list:
mqtt:
settings:
username: "your-mqtt-username"
password: "your-mqtt-password"
# Web Tool API Keys
web:
brave:
api_keys:
- "BSAyour-brave-api-key-1"
- "BSAyour-brave-api-key-2" # Optional: Multiple keys for failover
tavily:
api_keys:
- "tvly-your-tavily-api-key" # Single key in array format
perplexity:
api_keys:
- "pplx-your-perplexity-api-key" # Single key in array format
glm_search:
api_key: "your-glm-search-api-key" # GLMSearch uses single key format (not array)
baidu_search:
api_key: "your-baidu-search-api-key"
# Skills Registry Tokens
skills:
github:
token: "your-github-token"
clawhub:
auth_token: "your-clawhub-auth-token"Create or copy the security file:
cp security.example.yml ~/.picoclaw/.security.ymlEdit ~/.picoclaw/.security.yml and replace placeholder values with your actual API keys and tokens.
chmod 600 ~/.picoclaw/.security.ymlYou can now remove sensitive fields from config.json since they're loaded from .security.yml:
Before:
{
"model_list": [
{
"model_name": "gpt-5.4",
"model": "openai/gpt-5.4",
"api_base": "https://api.openai.com/v1",
"api_keys": ["sk-your-actual-api-key-here"]
}
],
"channel_list": {
"telegram": {
"enabled": true,
"type": "telegram",
"token": "1234567890:ABCdefGHIjklMNOpqrsTUVwxyz"
}
}
}After:
{
"model_list": [
{
"model_name": "gpt-5.4",
"model": "openai/gpt-5.4",
"api_base": "https://api.openai.com/v1"
// api_key is now loaded from .security.yml
}
],
"channel_list": {
"telegram": {
"enabled": true,
"type": "telegram"
// token is now loaded from .security.yml
}
}
}Restart PicoClaw and verify it loads correctly:
picoclaw --versionIn .security.yml:
model_list:
<model_name>:
api_keys:
- "key-1"
- "key-2"Mapping:
- Field
api_keys(array) maps to the model's API keys - The
<model_name>must match themodel_namefield inconfig.json - Supports indexed names (e.g., "gpt-5.4:0") - the system will also try the base name ("gpt-5.4")
Each channel maps its fields directly:
In .security.yml:
channels:
telegram:
token: "value"
feishu:
app_secret: "value"
encrypt_key: "value"
verification_token: "value"
discord:
token: "value"Mapping:
channels.telegram.token→config.channels.telegram.tokenchannels.feishu.app_secret→config.channels.feishu.app_secret- etc.
Channels that use a settings block (e.g. MQTT) use the channel_list key instead:
channel_list:
mqtt:
settings:
username: "value"
password: "value"channel_list.mqtt.settings.username→config.channel_list.mqtt.settings.usernamechannel_list.mqtt.settings.password→config.channel_list.mqtt.settings.password
Brave, Tavily, Perplexity:
web:
brave:
api_keys:
- "key-1"
- "key-2"- Use
api_keys(plural) array format
GLMSearch:
web:
glm_search:
api_key: "single-key-here"- Use
api_key(singular) single string format
BaiduSearch:
web:
baidu_search:
api_key: "your-key"- Use
api_key(singular) single string format
In .security.yml:
skills:
github:
token: "value"
clawhub:
auth_token: "value"Use array format with one element:
model_list:
gpt-5.4:
api_keys:
- "sk-your-key"Use array format with multiple elements:
model_list:
gpt-5.4:
api_keys:
- "sk-your-key-1"
- "sk-your-key-2"
- "sk-your-key-3"Benefits:
- Load balancing: Requests are distributed across multiple keys
- Failover: Automatic switching to another key if one fails
- Rate limit management: Distribute usage across multiple keys
- High availability: Reduce downtime during API provider issues
web:
brave:
api_keys:
- "BSA-your-key"web:
brave:
api_keys:
- "BSA-key-1"
- "BSA-key-2"web:
glm_search:
api_key: "your-glm-key" # Single string (NOT array)
baidu_search:
api_key: "your-baidu-key" # Single string (NOT array)The system supports intelligent model name matching in .security.yml:
config.json:
{
"model_name": "gpt-5.4:0"
}.security.yml (exact match with index):
model_list:
gpt-5.4:0:
api_keys: ["key-1"]config.json:
{
"model_name": "gpt-5.4:0"
}.security.yml (base name without index):
model_list:
gpt-5.4:
api_keys: ["key-1", "key-2"]Both methods work. The base name match allows you to use simpler keys in .security.yml even when your config uses indexed model names for load balancing.
The system maintains full backward compatibility:
- Direct values: You can still use direct values in
config.json(not recommended for production) - Mixed usage: You can have some fields in
.security.ymland others inconfig.json - Optional security file: If
.security.ymldoesn't exist, the system will only use values fromconfig.json - Override behavior: If a field exists in both files,
.security.ymlvalue takes precedence
You can override any security value using environment variables:
For models:
export PICOCLAW_CHANNELS_TELEGRAM_TOKEN="token-from-env"For channels:
export PICOCLAW_CHANNELS_TELEGRAM_TOKEN="token-from-env"
export PICOCLAW_CHANNELS_FEISHU_APP_SECRET="secret-from-env"For web tools:
export PICOCLAW_TOOLS_WEB_BRAVE_API_KEY="key-from-env"
export PICOCLAW_TOOLS_WEB_BAIDU_API_KEY="baidu-key-from-env"Environment variables have the highest priority and will override both config.json and .security.yml values.
The pattern is: PICOCLAW_<SECTION>_<KEY>_<FIELD> with underscores separating path segments and converted to uppercase.
- Never commit
.security.ymlto version control - Add to .gitignore: Ensure
.security.ymlis in your.gitignorefile - Set file permissions:
chmod 600 ~/.picoclaw/.security.yml - Use different keys for different environments (dev, staging, production)
- Rotate keys regularly and update
.security.yml - Backup securely: Encrypt backups containing
.security.yml. Note that config migrations automatically create date-stamped backups (e.g.,config.json.20260330.bakand.security.yml.20260330.bak) - Review access: Ensure only authorized users have read access to the file
func loadSecurityConfig(securityPath string) (*SecurityConfig, error)Loads the security configuration from .security.yml. Returns an empty SecurityConfig if the file doesn't exist.
func saveSecurityConfig(securityPath string, sec *SecurityConfig) errorSaves the security configuration to .security.yml with 0o600 permissions.
func applySecurityConfig(cfg *Config, sec *SecurityConfig) errorApplies security configuration to the main config by copying values from .security.yml to the corresponding fields in the config.
func securityPath(configPath string) stringReturns the path to .security.yml relative to the config file.
{
"version": 3,
"agents": {
"defaults": {
"workspace": "~/picoclaw-workspace",
"model_name": "gpt-5.4"
}
},
"model_list": [
{
"model_name": "gpt-5.4",
"model": "openai/gpt-5.4",
"api_base": "https://api.openai.com/v1"
},
{
"model_name": "claude-sonnet-4.6",
"model": "anthropic/claude-sonnet-4.6",
"api_base": "https://api.anthropic.com/v1"
}
],
"channel_list": {
"telegram": {
"enabled": true,
"type": "telegram"
}
},
"tools": {
"web": {
"brave": {
"enabled": true
}
}
}
}model_list:
gpt-5.4:
api_keys:
- "sk-proj-actual-openai-key-1"
- "sk-proj-actual-openai-key-2"
claude-sonnet-4.6:
api_keys:
- "sk-ant-actual-anthropic-key"
channels:
telegram:
token: "1234567890:ABCdefGHIjklMNOpqrsTUVwxyz"
web:
brave:
api_keys:
- "BSAactualbravekey-1"
- "BSAactualbravekey-2"
tavily:
api_keys:
- "tvly-your-tavily-key"
glm_search:
api_key: "your-glm-key"
baidu_search:
api_key: "your-baidu-key"Run the security configuration tests:
go test ./pkg/config -run TestSecurityConfig- Verify
.security.ymlexists in the same directory asconfig.json - Check the YAML syntax is valid (use a YAML validator)
- Ensure file permissions allow reading
- Ensure the model name in
config.jsonmatches exactly in.security.yml - Check that the
model_listsection exists in.security.yml - For models with indexed names (e.g., "gpt-5.4:0"), ensure the exact name is used or check the base name without index
- Verify the YAML structure is correct (proper indentation)
- Ensure you're using
api_keys(plural) in.security.ymlfor models and web tools (except GLMSearch/BaiduSearch) - Check that the array format is correct in YAML (proper indentation with dashes)
- Remember: Models, Brave, Tavily, Perplexity MUST use
api_keys(array format) - GLMSearch and BaiduSearch MUST use
api_key(single string format)
- Verify all API keys in the
api_keysarray are valid - Check that all keys have the same rate limits and permissions
- Monitor logs to see which keys are being used and failing
- Ensure the
api_keysarray is properly formatted in YAML
- Check that
.security.ymlis in the same directory asconfig.json - Verify the file permissions allow reading (
chmod 600 ~/.picoclaw/.security.yml) - Ensure the YAML structure matches the expected format
- Check for typos in field names (case-sensitive)
- Verify the model/channel names match exactly (case-sensitive)
The system automatically creates a date-stamped backup before saving a migrated config (e.g., config.json.20260330.bak and .security.yml.20260330.bak). If you prefer a manual backup:
cp ~/.picoclaw/config.json ~/.picoclaw/config.json.backupcp security.example.yml ~/.picoclaw/.security.ymlEdit ~/.picoclaw/.security.yml and replace placeholder values with your actual keys.
Remove or comment out sensitive fields from config.json:
api_keyfields frommodel_listentriestokenfields fromchannelsapi_keyfields fromtools.webtoken/auth_tokenfields fromtools.skills
chmod 600 ~/.picoclaw/.security.ymlpicoclaw --versionTest your models and channels to ensure everything works correctly.
If everything works, you can delete the backups:
rm ~/.picoclaw/config.json.backup
# Also remove auto-generated date-stamped backups if desired:
rm ~/.picoclaw/config.json.20*.bak ~/.picoclaw/.security.yml.20*.bakPicoClaw supports encrypting API keys in the security file for additional protection.
- Set a passphrase via environment variable:
export PICOCLAW_CREDENTIAL_PASSPHRASE="your-secure-passphrase"- When saving config, API keys will be encrypted automatically:
SaveConfig(path, config)Encrypted keys are stored as:
model_list:
gpt-5.4:
api_keys:
- "enc://encrypted-base64-string"The system automatically decrypts keys at runtime when loading the configuration.
- Additional layer of security
- Keys are encrypted at rest
- Passphrase can be managed separately from the config file
- Always backup your passphrase securely
- If you lose the passphrase, you'll lose access to encrypted keys
- Use a strong, unique passphrase
- Never commit the passphrase to version control