# Configuration Management Documentation
**Location:** `src/config/`
**Purpose:** Load, validate, and hot-reload configuration with Zod schemas
**Technology:** YAML configuration with environment variable interpolation and fs.watchFile hot-reload
---
## Table of Contents
1. [Overview](#overview)
2. [Configuration Structure](#configuration-structure)
3. [File Locations](#file-locations)
4. [Environment Variable Interpolation](#environment-variable-interpolation)
5. [Component Details](#component-details)
6. [Hot-Reload Mechanism](#hot-reload-mechanism)
7. [Validation with Zod](#validation-with-zod)
8. [Path Resolution](#path-resolution)
9. [Permission Approval System](#permission-approval-system)
10. [CLI Integration](#cli-integration)
---
## Overview
The Configuration subsystem provides **validated, hot-reloadable settings** using:
- **YAML Configuration File:** `config.yaml` with settings and team definitions
- **Environment Variable Interpolation:** `${VAR:-default}` syntax for dynamic configuration
- **Zod Validation:** Type-safe runtime validation with clear error messages
- **Hot-Reload:** Automatic reload on file changes (1s poll interval)
- **Path Resolution:** Relative paths resolved relative to config file
- **Default Config:** Built-in `default.config.yaml` template with extensive documentation
---
## Configuration Structure
### Example config.yaml
```yaml
settings:
sessionInitTimeout: 30000
responseTimeout: 120000
idleTimeout: 3600000
maxProcesses: 10
healthCheckInterval: 30000
httpPort: 1615
defaultTransport: http
hotReloadConfig: true # Enable automatic config reload
dashboard:
enabled: true
http: 3100
https: 0
host: localhost
database:
path: data/team-sessions.db
inMemory: false
teams:
team-alpha:
path: /Users/jenova/projects/alpha
description: Alpha team - Frontend development
idleTimeout: 3600000
grantPermission: yes
color: "#FF6B9D"
team-beta:
path: ./projects/beta
description: Beta team - Backend services
sessionInitTimeout: 45000
grantPermission: ask
color: "#4ECDC4"
```
---
## File Locations
### Search Order
1. **Environment Variable:** `IRIS_CONFIG_PATH`
```bash
export IRIS_CONFIG_PATH=/custom/path/config.yaml
iris start
```
2. **IRIS_HOME:** `$IRIS_HOME/config.yaml`
```bash
export IRIS_HOME=/opt/iris
# Looks for /opt/iris/config.yaml
```
3. **Default:** `~/.iris/config.yaml`
```bash
# Default location on macOS/Linux
/Users/jenova/.iris/config.yaml
```
### Directory Structure
```
~/.iris/ # IRIS_HOME (default)
āāā config.yaml # Main configuration
āāā data/
ā āāā team-sessions.db # SQLite database
āāā logs/ # Future: log files
```
### Creating Default Config
```bash
# Install command creates default config
iris install
# Or manually copy
cp src/default.config.yaml ~/.iris/config.yaml
```
---
## Environment Variable Interpolation
### Syntax
Iris configuration supports environment variable interpolation using the `${VAR}` syntax:
**Required Variable:**
```yaml
httpPort: ${IRIS_HTTP_PORT}
```
Throws error if `IRIS_HTTP_PORT` is not set.
**Optional Variable with Default:**
```yaml
httpPort: ${IRIS_HTTP_PORT:-1615}
```
Uses `1615` if `IRIS_HTTP_PORT` is not set.
### Common Use Cases
**Development vs Production:**
```yaml
settings:
idleTimeout: ${IRIS_IDLE_TIMEOUT:-300000} # 5 min dev, custom prod
maxProcesses: ${IRIS_MAX_PROCESSES:-10}
database:
path: ${IRIS_DB_PATH:-data/team-sessions.db}
```
**Dynamic Port Configuration:**
```yaml
settings:
httpPort: ${PORT:-1615}
dashboard:
http: ${DASHBOARD_PORT:-3100}
```
**Team-Specific Overrides:**
```yaml
teams:
team-production:
path: ${PROD_PATH:-/opt/app}
idleTimeout: ${PROD_TIMEOUT:-1800000}
```
### Environment File Example
Create `.env` file:
```bash
# Iris MCP Configuration
IRIS_HTTP_PORT=1615
IRIS_MAX_PROCESSES=20
IRIS_IDLE_TIMEOUT=600000
IRIS_DB_PATH=/var/lib/iris/sessions.db
```
Load before starting:
```bash
source .env
iris-mcp
```
---
## Component Details
### TeamsConfigManager (teams-config.ts)
**Responsibility:** Load, validate, and watch configuration file
**Constructor:**
```typescript
class TeamsConfigManager {
private config: TeamsConfig | null = null;
private configPath: string;
private watchCallback?: (config: TeamsConfig) => void;
constructor(configPath?: string) {
// Priority: provided > env var > default
if (configPath) {
this.configPath = configPath;
} else if (process.env.IRIS_CONFIG_PATH) {
this.configPath = resolve(process.env.IRIS_CONFIG_PATH);
} else {
this.configPath = getConfigPath(); // ~/.iris/config.yaml
}
ensureIrisHome(); // Create ~/.iris if doesn't exist
}
}
```
### Method: load()
**Purpose:** Load and validate configuration from file
**Flow:**
```
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā 1. Check if config file exists ā
ā if (!existsSync(configPath)): ā
ā Print installation instructions ā
ā exit(0) ā
āāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā 2. Read and parse YAML ā
ā content = readFileSync(configPath, 'utf8') ā
ā parsed = parseYaml(content) ā
ā ā Catches YAMLParseError for invalid syntax ā
āāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā 3. Interpolate environment variables ā
ā interpolated = interpolateObject(parsed, true) ā
ā ā Replaces ${VAR:-default} with env values ā
āāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā 4. Validate with Zod ā
ā validated = TeamsConfigSchema.parse(interpolated) ā
ā ā Catches ZodError with detailed path/message ā
āāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā 5. Check if teams configured ā
ā if (Object.keys(validated.teams).length === 0): ā
ā Print team configuration instructions ā
ā exit(0) ā
āāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā 6. Resolve team paths ā
ā configDir = dirname(resolve(configPath)) ā
ā for each team: ā
ā if (!isAbsolute(team.path)): ā
ā team.path = resolve(configDir, team.path) ā
āāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā 7. Validate team paths exist ā
ā for each team: ā
ā if (!existsSync(team.path)): ā
ā logger.warn("Team path does not exist", ...) ā
āāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā 8. Store and return ā
ā this.config = validated ā
ā logger.info("Configuration loaded successfully") ā
ā return config ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
```
**Error Handling:**
```typescript
try {
const parsed = parseYaml(content);
const interpolated = interpolateObject(parsed, true);
const validated = TeamsConfigSchema.parse(interpolated);
// ...
} catch (error) {
if (error instanceof z.ZodError) {
// Convert Zod errors to readable messages
const messages = error.errors.map(e =>
`${e.path.join('.')}: ${e.message}`
);
throw new ConfigurationError(
`Configuration validation failed:\n${messages.join('\n')}`
);
}
if (error instanceof Error && error.name === 'YAMLParseError') {
throw new ConfigurationError(
`Invalid YAML in configuration file: ${error.message}`
);
}
if (error instanceof Error && error.message.includes('Environment variable')) {
throw new ConfigurationError(error.message);
}
throw error;
}
```
---
## Hot-Reload Mechanism
### Overview
Hot reload allows Iris to automatically detect and apply configuration changes without restarting the server. This is controlled by the `hotReloadConfig` setting in your configuration file.
**Key Behavior:**
- Configuration changes are applied to **new sessions only**
- Existing sessions and running processes are **not affected**
- Hot reload is **opt-in** (disabled by default for safety)
### Enabling Hot Reload
Add `hotReloadConfig: true` to your settings:
```yaml
settings:
hotReloadConfig: true # Enable automatic config reload
sessionInitTimeout: 30000
maxProcesses: 10
```
When enabled, Iris will log:
```
[iris:cli] Hot reload enabled - watching config.yaml for changes
```
### Implementation
```typescript
watch(callback: (config: TeamsConfig) => void): void {
this.watchCallback = callback;
watchFile(this.configPath, { interval: 1000 }, () => {
logger.info('Configuration file changed, reloading...');
try {
const newConfig = this.load();
if (this.watchCallback) {
this.watchCallback(newConfig);
}
} catch (error) {
logger.error('Failed to reload configuration', error);
}
});
logger.info('Watching configuration file for changes');
}
```
**Polling Interval:** 1 second (configurable via `interval` option)
**Event Trigger:** File modification time (mtime) changes
**Why Polling?** Cross-platform compatibility (works on all OSes without native fs events)
### Usage in index.ts
```typescript
// Enable hot reload if configured
if (config.settings.hotReloadConfig) {
logger.info("Hot reload enabled - watching config.yaml for changes");
configManager.watch((newConfig) => {
logger.info(
{
teams: Object.keys(newConfig.teams),
maxProcesses: newConfig.settings.maxProcesses,
},
"Configuration reloaded - changes will apply to new sessions",
);
});
}
```
### What Gets Reloaded?
When the config file changes, Iris reloads:
- ā
Team definitions (added, removed, or modified teams)
- ā
Team paths and descriptions
- ā
Permission modes (`grantPermission`)
- ā
Timeout values (`sessionInitTimeout`, `idleTimeout`, etc.)
- ā
Process pool limits (`maxProcesses`)
- ā
Tool allowlists/denylists (`allowedTools`, `disallowedTools`)
- ā
System prompt customizations (`appendSystemPrompt`)
### What Doesn't Get Reloaded?
The following require a server restart:
- ā Dashboard settings (`dashboard.http`, `dashboard.https`)
- ā Database path (`database.path`)
- ā HTTP port (`settings.httpPort`)
- ā Transport mode (`settings.defaultTransport`)
- ā Wonder Logger configuration (`settings.wonderLoggerConfig`)
### Example: Adding a Team Without Restart
**Before** (config.yaml):
```yaml
settings:
hotReloadConfig: true
teams:
team-alpha:
path: /path/to/alpha
description: Alpha team
```
**Edit config.yaml** (while server is running):
```yaml
settings:
hotReloadConfig: true
teams:
team-alpha:
path: /path/to/alpha
description: Alpha team
team-beta: # Add new team
path: /path/to/beta
description: Beta team
grantPermission: ask
```
**Server logs:**
```
[config:teams] Configuration file changed, reloading...
[config:teams] Configuration loaded successfully { teams: ['team-alpha', 'team-beta'], maxProcesses: 10 }
[iris:cli] Configuration reloaded - changes will apply to new sessions { teams: ['team-alpha', 'team-beta'], maxProcesses: 10 }
```
**Result:**
- `team-beta` is immediately available for new sessions
- Existing `team-alpha` sessions continue unaffected
### Security Considerations
**Why Disabled by Default?**
Hot reload is disabled by default because:
1. **Unexpected Changes:** Configuration changes may not be immediately visible if processes are cached
2. **Permission Changes:** A team's permission mode could change mid-session
3. **Production Safety:** Production environments often want explicit restart for config changes
**When to Enable:**
Enable hot reload when:
- ā
Actively developing or testing
- ā
Frequently adding/removing teams
- ā
Experimenting with timeout values
- ā
Non-critical environments
**When to Disable:**
Keep hot reload disabled when:
- ā Production deployments
- ā You want explicit control over when config changes take effect
- ā Running in CI/CD environments
- ā Stability is critical
### Error Handling
**Invalid Configuration:**
If you save an invalid config while hot reload is enabled, the error is logged but the server continues with the last valid configuration:
```
[config:teams] Configuration file changed, reloading...
[config:teams] Failed to reload configuration {
err: ConfigurationError: Configuration validation failed:
teams.beta.path: String must contain at least 1 character(s)
}
```
The server continues running with the previous valid configuration.
**YAML Syntax Errors:**
```
[config:teams] Failed to reload configuration {
err: ConfigurationError: Invalid YAML in configuration file: bad indentation
}
```
Fix the YAML syntax and save again - the watcher will retry automatically.
---
## Validation with Zod
### Schema Definition
```typescript
import { z } from 'zod';
const IrisConfigSchema = z.object({
path: z.string().min(1, "Path cannot be empty"),
description: z.string(),
idleTimeout: z.number().positive().optional(),
sessionInitTimeout: z.number().positive().optional(),
color: z
.string()
.regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color")
.optional(),
// Phase 2: Remote execution via SSH
remote: z.string().optional(),
remoteOptions: z.object({
identity: z.string().optional(),
passphrase: z.string().optional(),
port: z.number().int().min(1).max(65535).optional(),
strictHostKeyChecking: z.boolean().optional(),
connectTimeout: z.number().positive().optional(),
serverAliveInterval: z.number().positive().optional(),
serverAliveCountMax: z.number().int().positive().optional(),
compression: z.boolean().optional(),
forwardAgent: z.boolean().optional(),
extraSshArgs: z.array(z.string()).optional(),
}).optional(),
claudePath: z.string().optional(),
// Reverse MCP tunneling
enableReverseMcp: z.boolean().optional(),
reverseMcpPort: z.number().int().min(1).max(65535).optional(),
allowHttp: z.boolean().optional(),
mcpConfigScript: z.string().optional(),
// Permission approval mode
grantPermission: z.enum(["yes", "no", "ask", "forward"]).optional().default("ask"),
// Tool allowlist/denylist
allowedTools: z.string().optional(),
disallowedTools: z.string().optional(),
// System prompt customization
appendSystemPrompt: z.string().optional(),
})
.refine((data) => {
if (data.remote && !data.claudePath) return false;
return true;
}, {
message: "claudePath is required when remote is specified",
path: ["claudePath"],
})
.refine((data) => {
if (data.enableReverseMcp && !data.remote) return false;
return true;
}, {
message: "enableReverseMcp requires remote execution to be configured",
path: ["enableReverseMcp"],
});
const TeamsConfigSchema = z.object({
settings: z.object({
sessionInitTimeout: z.number().positive().optional().default(30000),
spawnTimeout: z.number().positive().optional().default(20000),
responseTimeout: z.number().positive().optional().default(120000),
idleTimeout: z.number().positive().optional().default(3600000),
maxProcesses: z.number().int().min(1).max(50).optional().default(10),
healthCheckInterval: z.number().positive().optional().default(30000),
httpPort: z.number().int().min(1).max(65535).optional().default(1615),
defaultTransport: z.enum(["stdio", "http"]).optional().default("stdio"),
wonderLoggerConfig: z.string().optional().default("./wonder-logger.yaml"),
}),
dashboard: z.object({
enabled: z.boolean().default(true),
host: z.string().default("localhost"),
http: z.number().int().min(0).max(65535).optional().default(0),
https: z.number().int().min(0).max(65535).optional().default(3100),
selfsigned: z.boolean().optional().default(false),
certPath: z.string().optional(),
keyPath: z.string().optional(),
}).optional(),
database: z.object({
path: z.string().optional().default("data/team-sessions.db"),
inMemory: z.boolean().optional().default(false),
}).optional(),
teams: z.record(z.string(), IrisConfigSchema),
});
```
### Validation Benefits
**Type Safety:**
```typescript
const config: TeamsConfig = configManager.getConfig();
// TypeScript knows: config.settings.maxProcesses is number
```
**Runtime Validation:**
```yaml
settings:
maxProcesses: "ten" # ā Error: Expected number, received string
```
**Clear Error Messages:**
```
Configuration validation failed:
settings.maxProcesses: Expected number, received string
teams.alpha.path: String must contain at least 1 character(s)
teams.beta.color: Invalid hex color
teams.gamma.grantPermission: Invalid enum value. Expected 'yes' | 'no' | 'ask' | 'forward', received 'maybe'
```
---
## Path Resolution
### Absolute vs Relative Paths
**Absolute Path (Recommended):**
```yaml
teams:
alpha:
path: /Users/jenova/projects/alpha
```
**Relative Path (Resolved from config file location):**
```yaml
teams:
beta:
path: ../projects/beta
```
### Resolution Algorithm
```typescript
const configDir = dirname(resolve(this.configPath));
for (const [name, team] of Object.entries(validated.teams)) {
if (!isAbsolute(team.path)) {
// Resolve relative to config file directory
team.path = resolve(configDir, team.path);
}
// Validate resolved path exists
if (!existsSync(team.path)) {
logger.warn(`Team "${name}" path does not exist: ${team.path}`);
}
}
```
**Example:**
```
Config file: /Users/jenova/.iris/config.yaml
Team path: "./projects/alpha"
Resolved: /Users/jenova/.iris/projects/alpha
```
---
## Session MCP Configuration
### Overview
Session MCP configuration enables writing MCP config files for each Claude session, allowing the Iris MCP server to identify which team is making requests through sessionId-based URL routing.
**Key Benefits:**
- Session-specific routing: Each team's Claude instance gets a unique `/mcp/{sessionId}` endpoint
- Works for both local and remote teams
- Configurable MCP config file location
- Enables proper team identification in multi-team setups
### Configuration Fields
**`sessionMcpEnabled`** - Boolean (default: `false`)
Enable MCP config file writing for sessions. When enabled, Iris writes a config file before spawning Claude with the `--mcp-config` flag.
**`sessionMcpPath`** - String (default: `".claude/iris/mcp"`)
Directory path for MCP config files, relative to the team's project path. Config files are written to:
```
<team-path>/<sessionMcpPath>/iris-mcp-<sessionId>.json
```
### Global vs Team Settings
Both settings can be configured globally (in `settings`) and overridden per-team (in `teams.<team-name>`).
**Global configuration:**
```yaml
settings:
sessionMcpEnabled: true
sessionMcpPath: .claude/iris/mcp # All teams use this by default
```
**Team override:**
```yaml
teams:
team-custom:
path: /path/to/project
sessionMcpEnabled: true # Override global setting
sessionMcpPath: .custom/mcp/dir # Custom path for this team only
```
### Examples
**Enable for all teams globally:**
```yaml
settings:
sessionMcpEnabled: true
sessionMcpPath: .claude/iris/mcp
teams:
team-frontend:
path: /Users/you/projects/frontend
description: Frontend team
team-backend:
path: /Users/you/projects/backend
description: Backend team
```
**Enable for specific teams only:**
```yaml
settings:
sessionMcpEnabled: false # Disabled globally
teams:
team-frontend:
path: /Users/you/projects/frontend
sessionMcpEnabled: true # Enabled for this team only
team-backend:
path: /Users/you/projects/backend
# No sessionMcpEnabled = uses global default (false)
```
**Custom MCP directory path:**
```yaml
teams:
team-special:
path: /Users/you/projects/special
sessionMcpEnabled: true
sessionMcpPath: .mcp/configs # Custom directory
```
**Remote team with session MCP:**
```yaml
teams:
team-remote:
path: /home/user/projects/app
remote: ssh user@remote-host
claudePath: ~/.local/bin/claude
sessionMcpEnabled: true # Works with remote teams too
enableReverseMcp: true # Enable reverse tunnel for MCP communication
```
### How It Works
1. **Session Creation**: When a team session starts, Iris generates a unique sessionId
2. **MCP Config Writing**: If `sessionMcpEnabled: true`, Iris:
- Builds MCP config JSON with sessionId in URL: `http://localhost:1615/mcp/{sessionId}`
- Calls script (mcp-cp.sh/ps1 for local, mcp-scp.sh/ps1 for remote)
- Script writes config to `<team-path>/<sessionMcpPath>/iris-mcp-<sessionId>.json`
3. **Claude Spawn**: Iris spawns Claude with `--mcp-config <filepath>`
4. **Request Routing**: Claude connects to Iris using sessionId in URL path
5. **Cleanup**: On termination, Iris deletes the MCP config file
### File Locations
**Local team example:**
```
Team path: /Users/you/projects/frontend
SessionId: abc-123-def
sessionMcpPath: .claude/iris/mcp
File created: /Users/you/projects/frontend/.claude/iris/mcp/iris-mcp-abc-123-def.json
```
**Remote team example:**
```
Remote path: /home/user/projects/backend
SessionId: xyz-789-ghi
sessionMcpPath: .claude/iris/mcp
File created: /home/user/projects/backend/.claude/iris/mcp/iris-mcp-xyz-789-ghi.json
```
### Custom Scripts
You can provide custom scripts to handle MCP config file writing via the `mcpConfigScript` field:
```yaml
teams:
team-custom:
path: /path/to/project
sessionMcpEnabled: true
mcpConfigScript: /path/to/custom-mcp-writer.sh
```
**Script interface:**
- **Local**: `script <sessionId> <team-path> [sessionMcpPath]`
- **Remote**: `script <sessionId> <sshHost> <remote-team-path> [sessionMcpPath]`
- **Input**: JSON config on stdin
- **Output**: File path on stdout
See `examples/scripts/mcp-cp.sh` and `examples/scripts/mcp-scp.sh` for reference implementations.
### Security Considerations
**File Permissions:**
- Config directory: `700` (owner-only read/write/execute)
- Config files: `600` (owner-only read/write)
- Bundled scripts set these automatically
**Cleanup:**
- Config files are deleted when sessions terminate
- Orphaned files may remain if Iris crashes (safe to delete manually)
**Network:**
- Local teams connect to `http://localhost:1615/mcp/{sessionId}`
- Remote teams use reverse tunnel: `https://localhost:1615/mcp/{sessionId}` (if `enableReverseMcp: true`)
### Troubleshooting
**Config file not created:**
- Check `sessionMcpEnabled: true` is set
- Verify team path exists and is writable
- Check Iris logs for script execution errors
**Permission denied:**
- Ensure scripts have execute permission: `chmod +x examples/scripts/mcp-*.sh`
- For remote: verify SSH key authentication works
**sessionId routing not working:**
- Confirm `--mcp-config` flag appears in launch command (check logs)
- Verify config file contains correct URL with sessionId
- Check Iris HTTP server is listening on correct port
---
## Permission Approval System
### grantPermission Field
The `grantPermission` field controls how Claude handles permission requests for file operations and tool usage. This is **Phase 1** (schema only) - the implementation is planned for a future release.
**Type:** `enum ["yes", "no", "ask", "forward"]`
**Default:** `"ask"` (manual approval for safety)
### Permission Modes
**`yes` - Auto-Approve**
```yaml
teams:
team-dev:
grantPermission: yes
```
- All Claude actions are automatically approved
- No user interaction required
- **Use case:** Trusted development environments
- **Warning:** Claude has full file system access
**`no` - Auto-Deny**
```yaml
teams:
team-readonly:
grantPermission: no
```
- All Claude actions are automatically denied
- Claude can only read files, not modify
- **Use case:** Read-only analysis, code review teams
- **Note:** May limit Claude's effectiveness
**`ask` - Interactive Prompt (Default)**
```yaml
teams:
team-prod:
grantPermission: ask
```
- User is prompted for each action
- Interactive approval via terminal/UI
- **Use case:** Production environments, sensitive codebases
- **Note:** Requires user presence
**`forward` - Relay to Calling Team**
```yaml
teams:
team-remote:
grantPermission: forward
```
- Permission request is forwarded to the calling team
- Useful for remote execution scenarios
- **Use case:** Cross-team coordination with delegation
- **Note:** Requires Reverse MCP to be enabled
### Configuration Examples
**Development Team (Trusted):**
```yaml
team-frontend:
path: /Users/you/projects/frontend
description: Frontend development team
grantPermission: yes # Auto-approve all actions
```
**Production Team (Careful):**
```yaml
team-prod:
path: /opt/production/app
description: Production deployment team
grantPermission: ask # Prompt for each action
```
**Read-Only Analysis:**
```yaml
team-security:
path: /Users/you/projects/audit
description: Security audit team (read-only)
grantPermission: no # Deny all write operations
```
**Remote Execution with Delegation:**
```yaml
team-remote:
remote: ssh user@remote-host
claudePath: ~/.local/bin/claude
path: /home/user/projects/app
grantPermission: forward # Forward to calling team
enableReverseMcp: true
```
**Old (deprecated):**
```yaml
teams:
team-alpha:
```
**New (recommended):**
```yaml
teams:
team-alpha:
grantPermission: yes # Auto-approve (explicit)
```
**Compatibility:** Both fields are supported during the transition period. `grantPermission` takes precedence if both are set.
---
## MCP Configuration File System
### Overview
When `enableReverseMcp: true` is set, Iris writes MCP configuration files to enable Claude Code instances to communicate back with the Iris MCP server. The configuration file contains server connection details and must be passed to Claude via the `--mcp-config` flag.
**Architecture:** All filesystem operations are delegated to external shell scripts, keeping TypeScript code free of direct file I/O. TypeScript streams JSON to script stdin, scripts handle file writing and return file paths via stdout.
### Default Scripts
Iris provides four bundled scripts in `examples/scripts/`:
**Local Execution (Unix):**
- `mcp-cp.sh` - Writes config to team's `.claude/iris/mcp` directory
- Location: `<team-path>/.claude/iris/mcp/iris-mcp-{sessionId}.json`
- Creates directory with `chmod 700`, sets file to `chmod 600` (owner-only)
**Local Execution (Windows):**
- `mcp-cp.ps1` - PowerShell version for Windows
- Location: `<team-path>\.claude\iris\mcp\iris-mcp-{sessionId}.json`
- Creates directory and sets ACLs for owner-only access
**Remote Execution via SCP (Unix):**
- `mcp-scp.sh` - Writes to local temp, SCPs to remote host, cleans up local file
- Remote location: `<remote-team-path>/.claude/iris/mcp/iris-mcp-{sessionId}.json`
- Creates remote directory with `chmod 700`, sets file to `chmod 600`
**Remote Execution via SCP (Windows):**
- `mcp-scp.ps1` - PowerShell version for Windows
- Uses OpenSSH for Windows (ssh, scp commands)
- Same remote location as Unix version
### Script Interface
All scripts follow the same contract:
**Input (stdin):** JSON configuration object
```json
{
"mcpServers": {
"iris": {
"command": "node",
"args": ["/path/to/iris-mcp/dist/index.js"],
"env": {
"IRIS_REVERSE_MCP_SESSION_ID": "abc123"
}
}
}
}
```
**Output (stdout):** File path where config was written
```
/path/to/team/.claude/iris/mcp/iris-mcp-abc123.json
```
**Arguments:**
- **Local scripts**: `<sessionId> <team-path>`
- **Remote scripts**: `<sessionId> <sshHost> <remote-team-path>`
### Custom Scripts
Users can provide custom scripts via the `mcpConfigScript` field:
```yaml
teams:
team-custom:
path: /path/to/project
enableReverseMcp: true
mcpConfigScript: /path/to/custom-mcp-writer.sh
```
**Requirements:**
1. Script must accept JSON on stdin
2. Script must output file path to stdout (last non-empty line)
3. Script must exit with code 0 on success
4. For remote teams, script receives `<sessionId> <sshHost> <remote-team-path>` args
5. For local teams, script receives `<sessionId> <team-path>` args
**Example Custom Script:**
```bash
#!/usr/bin/env bash
# custom-mcp-writer.sh - Write to team's .claude/iris/mcp directory
SESSION_ID="$1"
TEAM_PATH="$2"
if [ -z "$SESSION_ID" ] || [ -z "$TEAM_PATH" ]; then
echo "ERROR: Session ID and team path required" >&2
exit 1
fi
# Create .claude/iris/mcp directory
MCP_DIR="${TEAM_PATH}/.claude/iris/mcp"
mkdir -p "$MCP_DIR"
chmod 700 "$MCP_DIR"
FILE_PATH="$MCP_DIR/iris-mcp-${SESSION_ID}.json"
# Read JSON from stdin
cat > "$FILE_PATH"
chmod 600 "$FILE_PATH"
# Output file path to stdout
echo "$FILE_PATH"
```
### Configuration Examples
**Default (bundled scripts):**
```yaml
teams:
team-local:
path: /path/to/project
enableReverseMcp: true
# Uses bundled mcp-cp.sh or mcp-cp.ps1
```
**Custom local directory:**
```yaml
teams:
team-local:
path: /path/to/project
enableReverseMcp: true
mcpConfigScript: /path/to/scripts/mcp-custom.sh
```
**Remote execution:**
```yaml
teams:
team-remote:
path: /remote/path/to/project
remote: user@remote-host
enableReverseMcp: true
# Uses bundled mcp-scp.sh or mcp-scp.ps1
```
**Remote with custom script:**
```yaml
teams:
team-remote:
path: /remote/path/to/project
remote: user@remote-host
enableReverseMcp: true
mcpConfigScript: /path/to/scripts/mcp-custom-scp.sh
```
### File Lifecycle
1. **Spawn:** Before spawning Claude process, transport writes MCP config file
2. **Execution:** File path passed to Claude via `--mcp-config <filepath>`
3. **Termination:** When transport terminates, config file is deleted
- **Local:** `fs.unlink()` via Node.js
- **Remote:** `ssh <host> rm -f <filepath>` for cleanup
### Security Considerations
**File Permissions:**
- Config files contain server connection details
- Default scripts set restrictive permissions (600 / owner-only)
- Remote directories created with 700 permissions
**Temporary Files:**
- Local configs written to `/tmp` or `%TEMP%` by default
- Remote scripts use `mktemp` for secure temp file creation
- Cleanup on both success and failure (trap EXIT)
**SSH Security:**
- Remote scripts use existing SSH configuration
- No passwords or keys stored in config files
- Relies on user's `~/.ssh/config` and agent
### Troubleshooting
**Script Not Found:**
```
Failed to execute MCP config script: ENOENT
```
- Verify script path in `mcpConfigScript` is absolute
- Ensure script has execute permissions (`chmod +x`)
**Permission Denied:**
```
MCP config script failed (exit code 1): Permission denied
```
- Check script execute permissions
- For remote: verify SSH key authentication works
- For remote: ensure remote directory is writable
**Config File Not Created:**
```
MCP config script did not output a file path
```
- Script must output file path to stdout
- Check script exits with code 0
- Verify script's stdout isn't being redirected
**Remote SCP Failures:**
```
scp: Connection refused
```
- Verify SSH connection works: `ssh <host> echo test`
- Check `remote` field is correctly formatted
- Ensure remote host has scp installed
---
## CLI Integration
### Installation Command
```bash
iris install
```
**Actions:**
1. Create `~/.iris/` directory
2. Copy `src/default.config.yaml` to `~/.iris/config.yaml`
3. Register Iris with Claude CLI config (`~/.claude/config.yaml`)
### Add Team Command
```bash
iris add-team <name> [path]
```
**Actions:**
1. Load existing config
2. Add new team entry
3. Write back to config file
4. Validate with Zod
**Example:**
```bash
iris add-team frontend /Users/jenova/projects/frontend
```
**Result:**
```yaml
teams:
frontend:
path: /Users/jenova/projects/frontend
description: frontend team
grantPermission: yes
```
---
## Configuration Reference
### Settings Section
```typescript
interface Settings {
sessionInitTimeout: number; // 30000ms (30s) - session file creation
spawnTimeout: number; // 20000ms (20s) - process spawn timeout
responseTimeout: number; // 120000ms (2min) - process health timeout
idleTimeout: number; // 3600000ms (1hr) - idle process cleanup
maxProcesses: number; // 10 - pool size limit (LRU eviction)
healthCheckInterval: number; // 30000ms (30s) - health check frequency
httpPort?: number; // 1615 - HTTP transport port (Phase 3)
defaultTransport?: "stdio" | "http"; // "stdio" - MCP transport mode
wonderLoggerConfig?: string; // "./wonder-logger.yaml" - observability config
hotReloadConfig?: boolean; // false - enable automatic config reload
sessionMcpEnabled?: boolean; // false - enable MCP config file writing
sessionMcpPath?: string; // ".claude/iris/mcp" - MCP config directory path
}
```
### Dashboard Section (Phase 2)
```typescript
interface Dashboard {
enabled: boolean; // true - enable web dashboard
host: string; // "localhost" - bind address
http: number; // 3100 - HTTP port (0 = disabled)
https: number; // 0 - HTTPS port (0 = disabled)
selfsigned: boolean; // false - use self-signed cert
certPath?: string; // Path to SSL certificate
keyPath?: string; // Path to SSL private key
}
```
### Database Section
```typescript
interface Database {
path?: string; // "data/team-sessions.db" - path to database file (relative to IRIS_HOME or absolute)
inMemory?: boolean; // false - use in-memory database (for testing)
}
```
**Path Resolution:**
- **Relative paths** are resolved relative to `IRIS_HOME` (default: `~/.iris`)
- **Absolute paths** are used as-is
- Default: `data/team-sessions.db` (resolves to `~/.iris/data/team-sessions.db`)
**In-Memory Mode:**
Set `inMemory: true` to use SQLite in-memory database. Useful for:
- Running tests without persisting data
- Temporary sessions
- Development environments
**Example - Custom Path:**
```yaml
database:
path: /var/lib/iris/sessions.db
```
**Example - In-Memory (Testing):**
```yaml
database:
inMemory: true
```
### Team Configuration
```typescript
interface IrisConfig {
path: string; // Absolute or relative path to project
description: string; // Human-readable description
idleTimeout?: number; // Optional override for this team
sessionInitTimeout?: number; // Optional override for this team
color?: string; // Hex color for UI (#FF6B9D)
// Remote execution
remote?: string; // SSH connection string (e.g., "user@host")
remoteOptions?: RemoteOptions; // SSH connection options
claudePath?: string; // Custom Claude CLI path (supports ~ expansion)
// Reverse MCP tunneling
enableReverseMcp?: boolean; // Enable reverse MCP tunnel for this team
reverseMcpPort?: number; // Port to tunnel (default: 1615)
allowHttp?: boolean; // Allow HTTP for reverse MCP (dev only)
mcpConfigScript?: string; // Custom script path for writing MCP config files
// Session MCP configuration
sessionMcpEnabled?: boolean; // Enable MCP config file writing (overrides global)
sessionMcpPath?: string; // MCP config directory path (overrides global)
// Permission approval mode
grantPermission?: "yes" | "no" | "ask" | "forward"; // Permission mode (default: "ask")
// Tool allowlist/denylist
allowedTools?: string; // Comma-separated list of allowed MCP tools
disallowedTools?: string; // Comma-separated list of denied MCP tools
// System prompt customization
appendSystemPrompt?: string; // Additional system prompt to append
}
```
**Per-Team Overrides:**
Teams can override global settings:
```yaml
settings:
idleTimeout: 3600000
teams:
long-running:
path: /path/to/project
idleTimeout: 86400000 # 24 hours (overrides global)
grantPermission: ask # Require approval for this team
```
---
## Error Messages
### Config File Not Found
```
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Iris MCP - Configuration Not Found ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Configuration file not found: /Users/jenova/.iris/config.yaml
Run the install command to create the default configuration:
$ iris install
This will:
1. Create the Iris MCP configuration file
2. Install Iris to your Claude CLI configuration
```
### No Teams Configured
```
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Iris MCP - No Teams Configured ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Configuration file: /Users/jenova/.iris/config.yaml
No teams are configured. Add teams to get started:
Add a team using current directory:
$ iris add-team <name>
Add a team with specific path:
$ iris add-team <name> /path/to/project
Show add-team help:
$ iris add-team --help
```
### Validation Errors
```
Configuration validation failed:
settings.maxProcesses: Number must be less than or equal to 50
settings.sessionInitTimeout: Expected number, received string
teams.alpha.path: String must contain at least 1 character(s)
teams.beta.color: Invalid hex color
teams.gamma.grantPermission: Invalid enum value. Expected 'yes' | 'no' | 'ask' | 'forward', received 'auto'
```
### Environment Variable Errors
```
Configuration validation failed:
Environment variable IRIS_HTTP_PORT is not set (required)
```
```
Configuration validation failed:
Environment variable PROD_PATH is not set and no default provided
```
---
## API Reference
### TeamsConfigManager
```typescript
class TeamsConfigManager {
constructor(configPath?: string);
// Load configuration from file
load(): TeamsConfig;
// Get current configuration (throws if not loaded)
getConfig(): TeamsConfig;
// Get configuration for specific team
getIrisConfig(teamName: string): IrisConfig | null;
// Get list of all team names
getTeamNames(): string[];
// Watch for config file changes
watch(callback: (config: TeamsConfig) => void): void;
}
```
### Helper Functions
```typescript
// Get default config path (~/.iris/config.yaml)
function getConfigPath(): string;
// Ensure IRIS_HOME directory exists
function ensureIrisHome(): void;
// Get singleton config manager
function getConfigManager(configPath?: string): TeamsConfigManager;
```
---
## Testing
### Unit Tests
```typescript
describe("TeamsConfigManager", () => {
it("should load valid configuration", () => {
const manager = new TeamsConfigManager("test-config.yaml");
const config = manager.load();
expect(config.settings.maxProcesses).toBe(10);
});
it("should reject invalid YAML", () => {
expect(() => {
manager.load();
}).toThrow("Invalid YAML");
});
it("should validate with Zod", () => {
expect(() => {
manager.load();
}).toThrow("Expected number, received string");
});
it("should interpolate environment variables", () => {
process.env.IRIS_HTTP_PORT = "8080";
const config = manager.load();
expect(config.settings.httpPort).toBe(8080);
});
});
```
---
## Future Enhancements
### 1. Dynamic Config Reload
**Current:** Hot-reload logs but doesn't update running processes
**Enhancement:** Apply configuration changes to running system
- Update process pool maxProcesses
- Adjust timeout values
- Add/remove teams dynamically
### 2. Config Validation CLI
```bash
iris config validate
```
Check config file without starting server
### 3. Config Schema Export
```bash
iris config schema > schema.json
```
Export JSON Schema for editor autocomplete
### 4. Permission Approval Implementation
**Current:** Schema only (grantPermission field defined)
**Planned:** Full implementation with:
- Interactive prompts for `ask` mode
- Permission forwarding for `forward` mode
- Audit logging for all permission decisions
- Dashboard UI for permission management
See [PERMISSION_APPROVAL_PLAN.md](./future/PERMISSION_APPROVAL_PLAN.md) for implementation details.
---
## Tech Writer Notes
**Coverage Areas:**
- YAML configuration format and structure
- Environment variable interpolation syntax (`${VAR:-default}`)
- Zod schema validation and error handling
- Hot-reload mechanism with fs.watchFile
- Path resolution (absolute vs relative)
- Permission approval system (grantPermission field)
- CLI integration (install, add-team commands)
- TeamsConfigManager API and methods
- Dashboard and database configuration options
**Keywords:** config.yaml, YAML, environment variables, interpolation, Zod validation, hot-reload, hotReloadConfig, grantPermission, permission approval, TeamsConfigManager, paths.ts, env-interpolation, teams configuration, MCP config scripts, mcpConfigScript, mcp-cp.sh, mcp-scp.sh, reverse MCP, ClaudeCommandBuilder, getMcpConfigPath, sessionMcpEnabled, sessionMcpPath, session MCP configuration, sessionId routing, local-transport.ts, ssh-transport.ts, mcp-config-writer.ts
**Last Updated:** 2025-10-18
**Change Context:** Added `sessionMcpEnabled` and `sessionMcpPath` configuration fields to enable session-specific MCP config file writing for both local and remote teams. This enables sessionId-based routing for proper team identification. Both fields can be set globally (settings) and overridden per-team. Updated all MCP config scripts (mcp-cp.sh, mcp-cp.ps1, mcp-scp.sh, mcp-scp.ps1) to accept optional sessionMcpPath parameter with `.claude/iris/mcp` as default. Updated transports (local-transport.ts, ssh-transport.ts) to use `sessionMcpEnabled` instead of `enableReverseMcp` for config file writing. Updated `mcp-config-writer.ts` and `ClaudeCommandBuilder.getMcpConfigPath()` to support configurable paths. Added comprehensive documentation section explaining session MCP configuration, examples, troubleshooting, and security considerations.
**Related Files:** GETTING_STARTED.md (config references), FEATURES.md (configuration management section), CLAUDE.md (config path references), README.md (config snippets), ARCHITECTURE.md (config system design), src/config/iris-config.ts (schema), src/transport/local-transport.ts, src/transport/ssh-transport.ts, src/utils/command-builder.ts, src/utils/mcp-config-writer.ts, examples/scripts/mcp-*.sh (script implementations)