Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions docs/src/content/docs/reference/safe-outputs-pull-requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -457,12 +457,15 @@ Protection covers three categories:

- `.github/` — covers all GitHub Actions workflows, Dependabot config, and other repository-level security settings.
- `.agents/` — covers generic agent instruction and configuration files stored in the `.agents/` directory.
- `.githooks/` — covers repository-tracked git hook scripts.
- `.husky/` — covers Husky-managed git hook scripts.

**4. Repository access control files** — matched by filename anywhere in the repository:
**4. Repository governance files** — matched by filename anywhere in the repository:

| File | Description |
|------|-------------|
| `CODEOWNERS` | Governs required code reviewers; valid at the repository root, `.github/`, or `docs/` |
| `DESIGN.md` | Defines persistent design-system guidance for coding agents |

> [!NOTE]
> Runtime manifests and access control files (`CODEOWNERS`) are matched by **basename only** (the filename without its directory path), so they are protected regardless of where they appear in the repository. Path-prefix rules (`.github/`, `.agents/`, `.claude/`, `.codex/`) match the full relative path from the repository root.
> Runtime manifests and governance files (`CODEOWNERS`, `DESIGN.md`) are matched by **basename only** (the filename without its directory path), so they are protected regardless of where they appear in the repository. Path-prefix rules (`.github/`, `.agents/`, `.githooks/`, `.husky/`, `.claude/`, `.codex/`) match the full relative path from the repository root.
15 changes: 14 additions & 1 deletion pkg/workflow/compiler_safe_outputs_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2255,7 +2255,7 @@ func TestProtectedFilesExclude(t *testing.T) {
name: "exclude AGENTS.md from create-pull-request",
excludeFiles: []string{"AGENTS.md"},
wantExcludedFromPF: []string{"AGENTS.md"},
wantPresentInPF: []string{"package.json", "go.mod", "CODEOWNERS"},
wantPresentInPF: []string{"package.json", "go.mod", "CODEOWNERS", "DESIGN.md"},
},
{
name: "exclude multiple files",
Expand Down Expand Up @@ -2388,4 +2388,17 @@ func TestProtectedFilesExcludePushToPRBranch(t *testing.T) {
}
assert.NotContains(t, pfStrings, "AGENTS.md", "AGENTS.md should be excluded from protected_files")
assert.Contains(t, pfStrings, "package.json", "package.json should still be in protected_files")

ppRaw, ok := pushConfig["protected_path_prefixes"]
require.True(t, ok, "should have protected_path_prefixes field")
ppAny, ok := ppRaw.([]any)
require.True(t, ok, "protected_path_prefixes should be a slice")
ppStrings := make([]string, 0, len(ppAny))
for _, v := range ppAny {
if s, ok := v.(string); ok {
ppStrings = append(ppStrings, s)
}
}
Comment on lines +2396 to +2401
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conversion of protected_path_prefixes from []any to []string silently drops non-string values (if s, ok := v.(string); ok { ... }). This can mask a serialization/type regression in the config. Consider asserting each element is a string (fail fast) and/or using a small helper to parse []any -> []string to avoid duplicating this pattern.

Copilot uses AI. Check for mistakes.
assert.Contains(t, ppStrings, ".githooks/", ".githooks/ should be in protected_path_prefixes by default")
assert.Contains(t, ppStrings, ".husky/", ".husky/ should be in protected_path_prefixes by default")
}
4 changes: 3 additions & 1 deletion pkg/workflow/runtime_definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ func init() {
// that files placed at the repo root or in "docs/" are equally protected.
var securityConfigFiles = []string{
"CODEOWNERS", // Governs required reviewers; valid at repo root, .github/, or docs/
"DESIGN.md", // Captures design-system source of truth consumed by coding agents
}

// getAllManifestFiles returns the deduplicated union of all manifest file names
Expand All @@ -212,8 +213,9 @@ func getAllManifestFiles(extra ...string) []string {
// additionally protected by filename (see securityConfigFiles) so that root-
// and docs/-level placements are covered too.
// ".agents/" covers generic agent instruction and configuration files.
// ".githooks/" and ".husky/" cover repository-tracked git hook scripts.
func getProtectedPathPrefixes(extra ...string) []string {
return mergeUnique([]string{".github/", ".agents/"}, extra...)
return mergeUnique([]string{".github/", ".agents/", ".githooks/", ".husky/"}, extra...)
}

// excludeFromSlice returns a new slice containing the items from base
Expand Down
3 changes: 3 additions & 0 deletions pkg/workflow/safe_outputs_config_generation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -549,9 +549,12 @@ func TestGenerateSafeOutputsConfigCreatePullRequestIncludesEngineManifests(t *te
protectedFiles := parseStringSliceAny(prConfig["protected_files"], nil)
assert.Contains(t, protectedFiles, "CLAUDE.md", "CLAUDE.md should be protected for Claude engine workflows")
assert.Contains(t, protectedFiles, "AGENTS.md", "AGENTS.md should be protected for Claude engine workflows")
assert.Contains(t, protectedFiles, "DESIGN.md", "DESIGN.md should be protected by default")

protectedPathPrefixes := parseStringSliceAny(prConfig["protected_path_prefixes"], nil)
assert.Contains(t, protectedPathPrefixes, ".claude/", ".claude/ should be protected for Claude engine workflows")
assert.Contains(t, protectedPathPrefixes, ".githooks/", ".githooks/ should be protected by default")
assert.Contains(t, protectedPathPrefixes, ".husky/", ".husky/ should be protected by default")
}

func TestGenerateSafeOutputsConfigCreatePullRequestAppliesProtectedFilesExclude(t *testing.T) {
Expand Down
Loading