| name | working-with-config-system |
| description | Guide to workspace configuration for environment variables, mount points, skills, MCP servers, secrets and network access at multiple levels |
| argument-hint | |
Working with the Config System
The config system manages workspace configuration for injecting environment variables, mounting directories and managing secrets into workspaces. This is different from runtime-specific configuration (e.g., Podman image settings).
What this config system controls:
- Environment variables to inject into workspace containers/VMs
- Additional directories to mount, with explicit host and container paths
- Skills directories to provide to agents inside the workspace
- MCP servers to configure in the agent (command-based and URL-based)
- Network access policies (allow all or deny with host exceptions)
- Secrets to inject into the workspace (secret names resolved from the CLI-managed store)
What this does NOT control:
- Runtime-specific settings (e.g., Podman container image, packages to install)
- See
/working-with-podman-runtime-config for runtime-specific configuration
Overview
The multi-level configuration system allows users to customize workspace settings at different levels:
- Workspace-level config (
.kaiden/workspace.json) - Shared project configuration committed to repository
- Can be configured using the
--workspace-configuration flag of the init command (path to directory containing workspace.json)
- Project-specific config (
~/.kdn/config/projects.json) - User's custom config for specific projects
- Global config (empty string
"" key in projects.json) - Settings applied to all projects
- Agent-specific config (
~/.kdn/config/agents.json) - Per-agent overrides (e.g., Claude, Goose)
These configurations control what gets injected into workspaces (environment variables, mounts), not how the workspace runtime is built or configured.
Agent Default Settings Files
In addition to the env/mount configuration above, kdn supports default settings files that are baked directly into the workspace image at init time.
Location: ~/.kdn/config/<agent>/ (one directory per agent name)
Any file placed in this directory is copied into the agent user's home directory (/home/agent/) inside the container image, preserving the directory structure. For example:
~/.kdn/config/claude/
└── .claude.json → /home/agent/.claude.json inside the image
This is distinct from agents.json:
agents.json — injects environment variables and mounts at runtime
config/<agent>/ — embeds dotfiles / settings files directly into the image at build time
Automatic Onboarding Configuration:
For supported agents (e.g., Claude), kdn automatically modifies settings files to skip onboarding prompts:
- Files are read from
config/<agent>/ (e.g., config/claude/.claude.json)
- If the agent is registered, its
SkipOnboarding() method is called
- The agent automatically adds necessary flags (e.g.,
hasCompletedOnboarding, hasTrustDialogAccepted)
- Modified settings are embedded into the container image
This means you can optionally customize agent preferences (theme, etc.) in the settings files, and kdn will automatically add the onboarding flags.
Model Configuration:
When the --model flag is provided during init, kdn does two things with the model:
-
Persists it in instance data: The model ID is stored in InstanceData.Model and saved to instances.json. It can be retrieved via instance.GetModel() and is shown in init verbose output and kdn list (MODEL column and JSON model field).
-
Writes it to agent settings: Calls the agent's SetModel() method to configure the model in the agent-specific settings file baked into the container image:
- Claude:
model field in .claude/settings.json
- Goose:
GOOSE_MODEL field in .config/goose/config.yaml
- Cursor:
model object in .cursor/cli-config.json
- OpenCode:
model field in .config/opencode/opencode.json
The --model flag takes precedence over any model already defined in the settings files. If no model is specified, GetModel() returns an empty string and the model field is omitted from JSON output.
Provider Configuration (OpenCode):
The --model flag supports a provider::model format that auto-configures the provider endpoint in SetModel(). Three formats are supported:
model — plain model ID, no provider configuration
provider::model — auto-configures provider with its known default base URL
provider::model::baseURL — auto-configures provider with a custom base URL
The model ID is stored as provider/model in the config. Known providers with default base URLs: ollama (http://host.containers.internal:11434/v1), ramalama (http://host.containers.internal:8080/v1). Unknown providers require the full provider::model::baseURL format. Localhost aliases in base URLs are auto-converted to host.containers.internal.
MCP Server Configuration:
When the merged workspace configuration contains an mcp field, the manager calls the agent's SetMCPServers() method to write the MCP servers into the agent settings:
- Claude: writes
mcpServers entries into .claude.json at the top-level (user scope)
- Command-based servers use
type: "stdio" with command, args, and env
- URL-based servers use
type: "sse" with url and optional headers
- Goose, Cursor, and OpenCode: no-op (MCP configuration through settings files not supported)
MCP servers from all configuration levels are merged before being passed to the agent, with higher-precedence levels overriding lower ones by server name.
Implementation: manager.readAgentSettings(storageDir, agentName) in pkg/instances/manager.go walks this directory and returns a map[string]agent.SettingsFile (relative forward-slash path → settings file with content and executable flag). If the agent is registered in the agent registry, the manager calls the agent's SkipOnboarding() method to modify the settings. If a model ID is provided, the manager then calls the agent's SetModel() method. If the merged config contains MCP servers, the manager calls the agent's SetMCPServers() method. The final map is passed to the runtime via runtime.CreateParams.AgentSettings. The Podman runtime writes the files into the build context and adds a COPY --chown=agent:agent agent-settings/. /home/agent/ instruction to the Containerfile. The model is also stored directly in the instance struct and persisted in instances.json via InstanceData.Model.
Key Components
- Config Interface (
pkg/config/config.go): Interface for managing configuration directories
- ConfigMerger (
pkg/config/merger.go): Merges multiple WorkspaceConfiguration objects
- AgentConfigLoader (
pkg/config/agents.go): Loads agent-specific configuration
- ProjectConfigLoader (
pkg/config/projects.go): Loads project and global configuration
- Manager Integration (
pkg/instances/manager.go): Handles config loading and merging during instance creation
- WorkspaceConfiguration Model: Imported from
github.com/openkaiden/kdn-api/workspace-configuration/go
Configuration File Locations
All user-specific configuration files are stored under the storage directory (default: ~/.kdn, configurable via --storage flag or KDN_STORAGE environment variable):
- Agent configs:
<storage-dir>/config/agents.json
- Project configs:
<storage-dir>/config/projects.json
- Workspace configs:
.kaiden/workspace.json (in workspace directory)
- Created/configured via
kdn init --workspace-configuration <directory-path>
Configuration Precedence
Configurations are merged from lowest to highest priority (highest wins):
- Agent-specific configuration (from
agents.json) - HIGHEST PRIORITY
- Project-specific configuration (from
projects.json using project ID)
- Global project configuration (from
projects.json using empty string "" key)
- Workspace-level configuration (from
.kaiden/workspace.json) - LOWEST PRIORITY
Configuration Structure
Workspace Configuration (workspace.json)
The workspace.json file controls what gets injected into the workspace:
{
"environment": [
{
"name": "DEBUG",
"value": "true"
},
{
"name": "API_KEY",
"secret": "github-token"
}
],
"mounts": [
{"host": "$SOURCES/../main", "target": "$SOURCES/../main"},
{"host": "$HOME/.ssh", "target": "$HOME/.ssh", "ro": true},
{"host": "$HOME/.gitconfig", "target": "$HOME/.gitconfig", "ro": true},
{"host": "/absolute/path/to/data", "target": "/workspace/data", "ro": true}
],
"skills": [
"/absolute/path/to/commit-skill",
"$HOME/review-skill"
],
"mcp": {
"commands": [
{
"name": "my-tool",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace/sources"],
"env": {"NODE_ENV": "production"}
}
],
"servers": [
{
"name": "remote-api",
"url": "https://api.example.com/mcp",
"headers": {"Authorization": "Bearer token123"}
}
]
},
"network": {
"mode": "deny",
"hosts": ["api.github.com"]
},
"secrets": [
"my-github-token",
"my-api-key"
]
}
Creating workspace configuration:
Use the --workspace-configuration flag with the init command to specify a directory containing workspace.json:
kdn init /path/to/workspace --workspace-configuration /path/to/config-dir
Fields:
environment - Environment variables to set in the workspace (optional)
name - Variable name (must be valid Unix environment variable name)
value - Hardcoded value (mutually exclusive with secret, empty strings allowed)
secret - Secret reference (mutually exclusive with value, cannot be empty)
mounts - List of directories to mount into the workspace (optional)
- Each entry is an object with
host, target, and optional ro fields
host - Path on the host filesystem (absolute path, or starts with $SOURCES or $HOME)
target - Path inside the container (absolute path, or starts with $SOURCES or $HOME)
ro - If true, mount is read-only (optional, defaults to read-write)
$SOURCES expands to the workspace sources directory on host, /workspace/sources in container
$HOME expands to the user's home directory on host, /home/<container-user> in container
skills - List of skill directories to provide to the agent (optional)
- Each entry is a path to a single skill directory on the host (containing a
SKILL.md and related files)
- Paths must be absolute or start with
$HOME ($SOURCES is not supported)
- Each directory is mounted read-only into the agent's skills directory using the directory's basename as the skill name
- The target path is agent-specific (e.g.,
$HOME/.claude/skills/<basename>/ for Claude Code)
mcp - MCP server configuration to inject into the agent's settings (optional)
commands - List of local command-based MCP servers (stdio transport)
name - Unique server name (required, must be unique across both commands and servers)
command - Executable to launch (required, e.g., npx, python3)
args - Arguments to pass to the command (optional)
env - Environment variables for the process (optional)
servers - List of remote URL-based MCP servers (SSE transport)
name - Unique server name (required, must be unique across both commands and servers)
url - SSE endpoint URL (required)
headers - HTTP headers to send with requests, e.g., for auth (optional)
- Names must be globally unique across both
commands and servers because agents flatten both lists into a single mcpServers map
network - Network access policy for the workspace (optional)
mode - Access mode: "allow" (permit all) or "deny" (block all except listed hosts). Defaults to "deny"
hosts - List of hostnames to allow in deny mode (optional, must not be set when mode is "allow")
secrets - List of secret names to inject into the workspace (optional)
- Each entry is a string: the name of a secret previously created with
kdn secret create
- Secret metadata (hosts, header, type, etc.) is resolved from the store at workspace creation time
- Secrets are distinct from the
secret field in environment variables, which references runtime secrets by name
Agent Configuration (agents.json)
Agent-specific overrides for environment variables and mounts:
{
"claude": {
"environment": [
{
"name": "DEBUG",
"value": "true"
}
],
"mounts": [
{"host": "$HOME/.claude-config", "target": "$HOME/.claude-config", "ro": true}
]
},
"goose": {
"environment": [
{
"name": "GOOSE_MODE",
"value": "verbose"
}
]
}
}
Project Configuration (projects.json)
Project-specific and global settings for environment variables and mounts:
{
"": {
"mounts": [
{"host": "$HOME/.gitconfig", "target": "$HOME/.gitconfig", "ro": true},
{"host": "$HOME/.ssh", "target": "$HOME/.ssh", "ro": true}
]
},
"https://github.com/openkaiden/kdn/": {
"environment": [
{
"name": "PROJECT_VAR",
"value": "project-value"
}
],
"mounts": [
{"host": "$SOURCES/../kaiden-common", "target": "$SOURCES/../kaiden-common"}
]
},
"/home/user/my/project": {
"environment": [
{
"name": "LOCAL_DEV",
"value": "true"
}
]
}
}
Special Keys:
- Empty string
"" represents global/default configuration applied to all projects
- Useful for common settings like SSH keys, Git config that should be mounted in all workspaces
- Project-specific configs override global config
Using the Config Interface
Loading Workspace Configuration
import (
"github.com/openkaiden/kdn/pkg/config"
workspace "github.com/openkaiden/kdn-api/workspace-configuration/go"
)
cfg, err := config.NewConfig("/path/to/workspace/.kaiden")
if err != nil {
return err
}
workspaceCfg, err := cfg.Load()
if err != nil {
if errors.Is(err, config.ErrConfigNotFound) {
} else if errors.Is(err, config.ErrInvalidConfig) {
} else {
return err
}
}
if workspaceCfg.Environment != nil {
for _, env := range *workspaceCfg.Environment {
}
}
if workspaceCfg.Mounts != nil {
for _, m := range *workspaceCfg.Mounts {
}
}
if workspaceCfg.Skills != nil {
for _, s := range *workspaceCfg.Skills {
}
}
Using the Multi-Level Config System
The Manager handles all configuration loading and merging automatically:
addedInstance, err := manager.Add(ctx, instances.AddOptions{
Instance: instance,
RuntimeType: "fake",
WorkspaceConfig: workspaceConfig,
Project: "custom-project",
Agent: "claude",
Model: "claude-sonnet-4",
})
The Manager's Add() method:
- Detects project ID (or uses custom override)
- Loads project config (global
"" + project-specific merged)
- Loads agent config (if agent name provided)
- Merges configs: workspace → global → project → agent
- Calls agent's
SkipOnboarding() if agent is registered
- Calls agent's
SetModel() if model is specified (takes precedence over settings)
- Converts
Skills entries into Mounts using the agent's SkillsDir() (agent-specific target path)
- Passes merged config to runtime for injection into workspace
Merging Behavior
- Environment variables: Later configs override earlier ones by name
- If the same variable appears in multiple configs, the one from the higher-precedence config wins
- Mounts: Deduplicated by
host+target pair (preserves order, removes duplicates)
- Skills: Deduplicated by path value (preserves order, base skills first then override)
- MCP servers: Merged separately for commands and servers, each deduplicated by
name
- Higher-precedence configs override lower ones when the same name appears in both
- Network: The base (lower-precedence) network policy is dominant
- If base has
allow mode, the base configuration is used regardless of the override
- If base has
deny and override has allow, the base configuration is used (overrides cannot loosen the policy)
- If both have
deny mode, the hosts from both are merged (deduplicated, base entries first)
- Secrets: Deduplicated by name (string); base entries come first, override adds new names that are not already present
Example Merge Flow:
Given:
- Workspace config:
DEBUG=workspace, WORKSPACE_VAR=value1
- Global config:
GLOBAL_VAR=global
- Project config:
DEBUG=project, PROJECT_VAR=value2
- Agent config:
DEBUG=agent, AGENT_VAR=value3
Result: DEBUG=agent, WORKSPACE_VAR=value1, GLOBAL_VAR=global, PROJECT_VAR=value2, AGENT_VAR=value3
Loading Configuration Programmatically
import "github.com/openkaiden/kdn/pkg/config"
projectLoader, err := config.NewProjectConfigLoader(storageDir)
projectConfig, err := projectLoader.Load("github.com/user/repo")
agentLoader, err := config.NewAgentConfigLoader(storageDir)
agentConfig, err := agentLoader.Load("claude")
merger := config.NewMerger()
merged := merger.Merge(workspaceConfig, projectConfig)
merged = merger.Merge(merged, agentConfig)
Configuration Validation
The Load() method automatically validates the configuration and returns ErrInvalidConfig if any of these rules are violated:
Environment Variables
- Name cannot be empty
- Name must be a valid Unix environment variable name (starts with letter or underscore, followed by letters, digits, or underscores)
- Exactly one of
value or secret must be defined
- Secret references cannot be empty strings
- Empty values are allowed (valid use case: set env var to empty string)
Mount Paths
- Each mount must have a non-empty
host and target
- Both
host and target must be absolute paths OR start with $SOURCES or $HOME
- Relative paths (e.g.,
../foo) are not allowed
$SOURCES-based container targets must not escape above /workspace
$HOME-based container targets must not escape above /home/<container-user>
Skills
- Each entry cannot be empty
- Each path must be an absolute path or start with
$HOME ($SOURCES is not supported)
- Duplicate paths (within or across config levels) are deduplicated by the merger
MCP Servers
- Command
name cannot be empty
- Command
command field cannot be empty
- Server
name cannot be empty
- Server
url field cannot be empty
- Names must be unique across both
commands and servers combined — a command and a server cannot share the same name, since all entries map to the same flat mcpServers key in the agent settings
Network
- If
mode is set, it must be a valid value ("allow" or "deny")
- If
mode is "allow", hosts must not be set (they are meaningless in allow mode)
- Host entries cannot be empty strings
Secrets
- Each entry (a secret name string) cannot be empty
- Secret names must be unique within the list — duplicate names are rejected
Error Handling
config.ErrInvalidPath - Configuration path is empty or invalid
config.ErrConfigNotFound - The workspace.json file is not found
config.ErrInvalidConfig - Configuration validation failed (includes detailed error message)
config.ErrInvalidAgentConfig - Agent configuration is invalid
config.ErrInvalidProjectConfig - Project configuration is invalid
Testing Multi-Level Configs
configDir := filepath.Join(storageDir, "config")
os.MkdirAll(configDir, 0755)
agentsJSON := `{"claude": {"environment": [{"name": "VAR", "value": "val"}]}}`
os.WriteFile(filepath.Join(configDir, "agents.json"), []byte(agentsJSON), 0644)
rootCmd.SetArgs([]string{"init", sourcesDir, "--runtime", "fake", "--agent", "claude"})
rootCmd.Execute()
Design Principles
- Configuration directory is NOT automatically created
- Missing configuration directory is treated as empty/default configuration
- All configurations are validated on load to catch errors early
- Configuration merging is handled by Manager, not commands
- Missing config files return empty configs (not errors)
- Invalid JSON or validation errors are reported
- All loaders follow the module design pattern
- Cross-platform compatible (uses
filepath.Join(), t.TempDir())
- Storage directory is configurable via
--storage flag or KDN_STORAGE env var
- Uses nested JSON structure for clarity and extensibility
- Model types are imported from external API package for consistency
Related Skills
/working-with-podman-runtime-config - Configure runtime-specific settings (Podman image, packages, etc.)
/working-with-instances-manager - Using the instances manager API
References
- Config Interface:
pkg/config/config.go
- ConfigMerger:
pkg/config/merger.go
- AgentConfigLoader:
pkg/config/agents.go
- ProjectConfigLoader:
pkg/config/projects.go
- Manager Integration:
pkg/instances/manager.go