docs/security/security_configuration.md
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:
.security.yml should be added to .gitignore~/.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.
.security.yml are automatically mapped to corresponding fields in the config.security.yml, it overrides the value in config.jsonconfig.json entirely (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"
# 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.yml
Edit ~/.picoclaw/.security.yml and replace placeholder values with your actual API keys and tokens.
chmod 600 ~/.picoclaw/.security.yml
You 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_key": "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 --version
In .security.yml:
model_list:
<model_name>:
api_keys:
- "key-1"
- "key-2"
Mapping:
api_keys (array) maps to the model's API keys<model_name> must match the model_name field in config.jsonEach 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_secretBrave, Tavily, Perplexity:
web:
brave:
api_keys:
- "key-1"
- "key-2"
api_keys (plural) array formatGLMSearch:
web:
glm_search:
api_key: "single-key-here"
api_key (singular) single string formatBaiduSearch:
web:
baidu_search:
api_key: "your-key"
api_key (singular) single string formatIn .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:
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:
config.json (not recommended for production).security.yml and others in config.json.security.yml doesn't exist, the system will only use values from config.json.security.yml value takes precedenceYou 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.
.security.yml to version control.security.yml is in your .gitignore filechmod 600 ~/.picoclaw/.security.yml.security.yml.security.yml. Note that config migrations automatically create date-stamped backups (e.g., config.json.20260330.bak and .security.yml.20260330.bak)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) error
Saves the security configuration to .security.yml with 0o600 permissions.
func applySecurityConfig(cfg *Config, sec *SecurityConfig) error
Applies security configuration to the main config by copying values from .security.yml to the corresponding fields in the config.
func securityPath(configPath string) string
Returns 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
.security.yml exists in the same directory as config.jsonconfig.json matches exactly in .security.ymlmodel_list section exists in .security.ymlapi_keys (plural) in .security.yml for models and web tools (except GLMSearch/BaiduSearch)api_keys (array format)api_key (single string format)api_keys array are validapi_keys array is properly formatted in YAML.security.yml is in the same directory as config.jsonchmod 600 ~/.picoclaw/.security.yml)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.backup
cp security.example.yml ~/.picoclaw/.security.yml
Edit ~/.picoclaw/.security.yml and replace placeholder values with your actual keys.
Remove or comment out sensitive fields from config.json:
api_key fields from model_list entriestoken fields from channelsapi_key fields from tools.webtoken/auth_token fields from tools.skillschmod 600 ~/.picoclaw/.security.yml
picoclaw --version
Test 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*.bak
PicoClaw supports encrypting API keys in the security file for additional protection.
export PICOCLAW_CREDENTIAL_PASSPHRASE="your-secure-passphrase"
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.