diff --git a/docs/src/content/docs/reference/safe-outputs-pull-requests.md b/docs/src/content/docs/reference/safe-outputs-pull-requests.md index 68a28e72a58..9006f76e546 100644 --- a/docs/src/content/docs/reference/safe-outputs-pull-requests.md +++ b/docs/src/content/docs/reference/safe-outputs-pull-requests.md @@ -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. diff --git a/pkg/workflow/compiler_safe_outputs_config_test.go b/pkg/workflow/compiler_safe_outputs_config_test.go index b395cafd290..6be7faf69b6 100644 --- a/pkg/workflow/compiler_safe_outputs_config_test.go +++ b/pkg/workflow/compiler_safe_outputs_config_test.go @@ -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", @@ -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) + } + } + 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") } diff --git a/pkg/workflow/runtime_definitions.go b/pkg/workflow/runtime_definitions.go index dd9502baf0d..71d36860f1c 100644 --- a/pkg/workflow/runtime_definitions.go +++ b/pkg/workflow/runtime_definitions.go @@ -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 @@ -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 diff --git a/pkg/workflow/safe_outputs_config_generation_test.go b/pkg/workflow/safe_outputs_config_generation_test.go index 26ffde8a690..20088c5a8ee 100644 --- a/pkg/workflow/safe_outputs_config_generation_test.go +++ b/pkg/workflow/safe_outputs_config_generation_test.go @@ -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) {