Skip to content
Merged
110 changes: 109 additions & 1 deletion .github/workflows/test-claude-create-issue.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/test-claude-create-issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:

engine:
id: claude

strict: true
safe-outputs:
create-issue:
title-prefix: "[claude-test] "
Expand Down
53 changes: 53 additions & 0 deletions docs/frontmatter.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The YAML frontmatter supports standard GitHub Actions properties plus additional
- `tools`: Available tools and MCP servers for the AI engine
- `cache`: Cache configuration for workflow dependencies
- `safe-outputs`: [Safe Output Processing](safe-outputs.md) for automatic issue creation and comment posting.
- `strict`: Enable strict mode to enforce deny-by-default permissions for engine and MCP servers

## Trigger Events (`on:`)

Expand Down Expand Up @@ -283,6 +284,58 @@ engine:
- "*.safe-domain.org"
```

## Strict Mode (`strict:`)

Strict mode enforces deny-by-default permissions for both engine and MCP servers even when no explicit permissions are configured. This provides a zero-trust security model that adheres to security best practices.

```yaml
strict: true # Enable strict mode (default: false)
```

### Behavior

When strict mode is enabled:

1. **No explicit network permissions**: Automatically enforces deny-all policy
```yaml
strict: true
engine: claude
# No engine.permissions.network specified
# Result: All network access is denied (same as empty allowed list)
```

2. **Explicit network permissions**: Uses the specified permissions normally
```yaml
strict: true
engine:
id: claude
permissions:
network:
allowed: ["api.github.com"]
# Result: Only api.github.com is accessible
```

3. **Strict mode disabled**: Maintains backwards-compatible behavior
```yaml
strict: false # or omitted entirely
engine: claude
# No engine.permissions.network specified
# Result: Unrestricted network access (backwards compatible)
```

### Use Cases

- **Security-first workflows**: When you want to ensure no accidental network access
- **Compliance requirements**: For environments requiring deny-by-default policies
- **Zero-trust environments**: When explicit permissions should always be required
- **Migration assistance**: Gradually migrate existing workflows to explicit permissions

### Compatibility

- Only applies to engines that support network permissions (currently Claude)
- Non-Claude engines ignore strict mode setting
- Backwards compatible when `strict: false` or omitted

## Safe Outputs Configuration (`safe-outputs:`)

See [Safe Outputs Processing](safe-outputs.md) for automatic issue creation, comment posting and other safe outputs.
Expand Down
5 changes: 5 additions & 0 deletions pkg/cli/templates/instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ The YAML frontmatter supports these fields:
- "*.trusted-domain.com"
```

- **`strict:`** - Enable strict mode for deny-by-default permissions (boolean, default: false)
```yaml
strict: true # Enforce deny-all network permissions when no explicit permissions set
```

- **`tools:`** - Tool configuration for coding agent
- `github:` - GitHub API tools
- `claude:` - Claude-specific tools
Expand Down
25 changes: 25 additions & 0 deletions pkg/parser/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,31 @@ func TestValidateMainWorkflowFrontmatterWithSchema(t *testing.T) {
wantErr: true,
errContains: "additional properties 'invalid_prop' not allowed",
},
{
name: "valid strict mode true",
frontmatter: map[string]any{
"on": "push",
"strict": true,
},
wantErr: false,
},
{
name: "valid strict mode false",
frontmatter: map[string]any{
"on": "push",
"strict": false,
},
wantErr: false,
},
{
name: "invalid strict mode as string",
frontmatter: map[string]any{
"on": "push",
"strict": "true",
},
wantErr: true,
errContains: "want boolean",
},
{
name: "valid claude engine with network permissions",
frontmatter: map[string]any{
Expand Down
4 changes: 4 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,10 @@
}
]
},
"strict": {
"type": "boolean",
"description": "Enable strict mode to enforce deny-by-default permissions for engine and MCP servers even when permissions are not explicitly set"
},
"safe-outputs": {
"type": "object",
"description": "Output configuration for automatic safe outputs",
Expand Down
35 changes: 32 additions & 3 deletions pkg/workflow/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,26 @@ func (c *Compiler) parseWorkflowFile(markdownPath string) (*WorkflowData, error)
// Extract AI engine setting from frontmatter
engineSetting, engineConfig := c.extractEngineConfig(result.Frontmatter)

// Extract strict mode setting from frontmatter
strictMode := c.extractStrictMode(result.Frontmatter)

// Apply strict mode: inject deny-all network permissions if strict mode is enabled
// and no explicit network permissions are configured
if strictMode && engineConfig != nil && engineConfig.ID == "claude" {
if engineConfig.Permissions == nil || engineConfig.Permissions.Network == nil {
// Initialize permissions structure if needed
if engineConfig.Permissions == nil {
engineConfig.Permissions = &EnginePermissions{}
}
if engineConfig.Permissions.Network == nil {
// Inject deny-all network permissions (empty allowed list)
engineConfig.Permissions.Network = &NetworkPermissions{
Allowed: []string{}, // Empty list means deny-all
}
}
}
}

// Override with command line AI engine setting if provided
if c.engineOverride != "" {
originalEngineSetting := engineSetting
Expand Down Expand Up @@ -702,7 +722,7 @@ func (c *Compiler) parseWorkflowFile(markdownPath string) (*WorkflowData, error)
}

// Apply defaults
c.applyDefaults(workflowData, markdownPath)
c.applyDefaults(workflowData, markdownPath, strictMode)

// Apply pull request draft filter if specified
c.applyPullRequestDraftFilter(workflowData, result.Frontmatter)
Expand Down Expand Up @@ -904,7 +924,7 @@ func (c *Compiler) extractCommandName(frontmatter map[string]any) string {
}

// applyDefaults applies default values for missing workflow sections
func (c *Compiler) applyDefaults(data *WorkflowData, markdownPath string) {
func (c *Compiler) applyDefaults(data *WorkflowData, markdownPath string, strictMode bool) {
// Check if this is a command trigger workflow (by checking if user specified "on.command")
isCommandTrigger := false
if data.On == "" {
Expand Down Expand Up @@ -1002,7 +1022,16 @@ func (c *Compiler) applyDefaults(data *WorkflowData, markdownPath string) {
}

if data.Permissions == "" {
data.Permissions = `permissions: read-all`
if strictMode {
// In strict mode, default to empty permissions instead of read-all
data.Permissions = `permissions: {}`
} else {
// Default behavior: use read-all permissions
data.Permissions = `permissions: read-all`
}
} else if strictMode {
// In strict mode, validate permissions and warn about write permissions
c.validatePermissionsInStrictMode(data.Permissions)
}

// Generate concurrency configuration using the dedicated concurrency module
Expand Down
Loading