From a7bb9e7d2872a18efb8b8462d8f1545b14b0977c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 20 Mar 2026 04:22:32 +0000
Subject: [PATCH 1/7] Initial plan


From 1263aebb7418ba42b74f2d628b0c71704c0a006a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 20 Mar 2026 04:28:05 +0000
Subject: [PATCH 2/7] Initial plan: skip permissions for staged safe outputs

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/faa27e25-cf6d-4c9f-84d7-d56982dd015b
---
 .github/workflows/smoke-codex.lock.yml | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml
index 7514dfdddff..a3c38f8c672 100644
--- a/.github/workflows/smoke-codex.lock.yml
+++ b/.github/workflows/smoke-codex.lock.yml
@@ -450,13 +450,20 @@ jobs:
               {
                 "description": "Add the 'smoked' label to the current pull request (can only be called once)",
                 "inputSchema": {
-                  "additionalProperties": true,
+                  "additionalProperties": false,
                   "properties": {
-                    "payload": {
-                      "description": "JSON-encoded payload to pass to the action",
+                    "labels": {
+                      "description": "The labels' name to be added. Must be separated with line breaks if there're multiple labels.",
+                      "type": "string"
+                    },
+                    "number": {
+                      "description": "The number of the issue or pull request.",
                       "type": "string"
                     }
                   },
+                  "required": [
+                    "labels"
+                  ],
                   "type": "object"
                 },
                 "name": "add_smoked_label"
@@ -1561,7 +1568,8 @@ jobs:
         env:
           GITHUB_TOKEN: ${{ github.token }}
         with:
-          payload: ${{ steps.process_safe_outputs.outputs.action_add_smoked_label_payload }}
+          labels: ${{ fromJSON(steps.process_safe_outputs.outputs.action_add_smoked_label_payload).labels }}
+          number: ${{ fromJSON(steps.process_safe_outputs.outputs.action_add_smoked_label_payload).number }}
       - name: Upload safe output items
         if: always()
         uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0

From cba5672062910a8d7847a1b5a6267b05f3f34db9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 20 Mar 2026 04:37:29 +0000
Subject: [PATCH 3/7] Skip permissions for staged safe outputs in
 ComputePermissionsForSafeOutputs

When a safe output handler is marked as staged (either globally via
safeOutputs.Staged or per-handler via BaseSafeOutputConfig.Staged),
it only emits preview output and does not make real GitHub API calls.
Therefore, such handlers should not contribute write permissions.

- Add isHandlerStaged() helper to check staged state
- Modify ComputePermissionsForSafeOutputs to skip permission merges
  for staged handlers
- Add TestComputePermissionsForSafeOutputs_Staged with 8 test cases

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/faa27e25-cf6d-4c9f-84d7-d56982dd015b
---
 pkg/workflow/safe_outputs_permissions.go      |  81 +++++++-----
 pkg/workflow/safe_outputs_permissions_test.go | 123 ++++++++++++++++++
 2 files changed, 170 insertions(+), 34 deletions(-)

diff --git a/pkg/workflow/safe_outputs_permissions.go b/pkg/workflow/safe_outputs_permissions.go
index 833eea6493c..4d46c710fa7 100644
--- a/pkg/workflow/safe_outputs_permissions.go
+++ b/pkg/workflow/safe_outputs_permissions.go
@@ -43,6 +43,14 @@ func stepsRequireIDToken(steps []any) bool {
 	return false
 }
 
+// isHandlerStaged returns true when a safe output handler is effectively staged
+// (i.e., it will only emit preview output, not make real API calls). A handler is
+// staged when either the global safe-outputs staged flag is true, or the
+// per-handler staged flag is true. Staged handlers do not require write permissions.
+func isHandlerStaged(globalStaged, handlerStaged bool) bool {
+	return globalStaged || handlerStaged
+}
+
 // ComputePermissionsForSafeOutputs computes the minimal required permissions
 // based on the configured safe-outputs. This function is used by both the
 // consolidated safe outputs job and the conclusion job to ensure they only
@@ -50,6 +58,8 @@ func stepsRequireIDToken(steps []any) bool {
 //
 // This implements the principle of least privilege by only including
 // permissions that are required by the configured safe outputs.
+// Handlers that are staged (globally or per-handler) are skipped because
+// staged mode only emits preview output and does not make any API calls.
 func ComputePermissionsForSafeOutputs(safeOutputs *SafeOutputsConfig) *Permissions {
 	if safeOutputs == nil {
 		safeOutputsPermissionsLog.Print("No safe outputs configured, returning empty permissions")
@@ -58,57 +68,60 @@ func ComputePermissionsForSafeOutputs(safeOutputs *SafeOutputsConfig) *Permissio
 
 	permissions := NewPermissions()
 
-	// Merge permissions for all handler-managed types
-	if safeOutputs.CreateIssues != nil {
+	// Merge permissions for all handler-managed types.
+	// Staged handlers are skipped because they do not make real API calls.
+	if safeOutputs.CreateIssues != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CreateIssues.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for create-issue")
 		permissions.Merge(NewPermissionsContentsReadIssuesWrite())
 	}
-	if safeOutputs.CreateDiscussions != nil {
+	if safeOutputs.CreateDiscussions != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CreateDiscussions.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for create-discussion")
 		permissions.Merge(NewPermissionsContentsReadIssuesWriteDiscussionsWrite())
 	}
-	if safeOutputs.AddComments != nil {
+	if safeOutputs.AddComments != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.AddComments.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for add-comment")
 		permissions.Merge(buildAddCommentPermissions(safeOutputs.AddComments))
 	}
-	if safeOutputs.CloseIssues != nil {
+	if safeOutputs.CloseIssues != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CloseIssues.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for close-issue")
 		permissions.Merge(NewPermissionsContentsReadIssuesWrite())
 	}
-	if safeOutputs.CloseDiscussions != nil {
+	if safeOutputs.CloseDiscussions != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CloseDiscussions.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for close-discussion")
 		permissions.Merge(NewPermissionsContentsReadDiscussionsWrite())
 	}
-	if safeOutputs.AddLabels != nil {
+	if safeOutputs.AddLabels != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.AddLabels.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for add-labels")
 		permissions.Merge(NewPermissionsContentsReadIssuesWritePRWrite())
 	}
-	if safeOutputs.RemoveLabels != nil {
+	if safeOutputs.RemoveLabels != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.RemoveLabels.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for remove-labels")
 		permissions.Merge(NewPermissionsContentsReadIssuesWritePRWrite())
 	}
-	if safeOutputs.UpdateIssues != nil {
+	if safeOutputs.UpdateIssues != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.UpdateIssues.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for update-issue")
 		permissions.Merge(NewPermissionsContentsReadIssuesWrite())
 	}
-	if safeOutputs.UpdateDiscussions != nil {
+	if safeOutputs.UpdateDiscussions != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.UpdateDiscussions.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for update-discussion")
 		permissions.Merge(NewPermissionsContentsReadDiscussionsWrite())
 	}
-	if safeOutputs.LinkSubIssue != nil {
+	if safeOutputs.LinkSubIssue != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.LinkSubIssue.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for link-sub-issue")
 		permissions.Merge(NewPermissionsContentsReadIssuesWrite())
 	}
-	if safeOutputs.UpdateRelease != nil {
+	if safeOutputs.UpdateRelease != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.UpdateRelease.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for update-release")
 		permissions.Merge(NewPermissionsContentsWrite())
 	}
-	if safeOutputs.CreatePullRequestReviewComments != nil || safeOutputs.SubmitPullRequestReview != nil ||
-		safeOutputs.ReplyToPullRequestReviewComment != nil || safeOutputs.ResolvePullRequestReviewThread != nil {
+	if (safeOutputs.CreatePullRequestReviewComments != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CreatePullRequestReviewComments.Staged)) ||
+		(safeOutputs.SubmitPullRequestReview != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.SubmitPullRequestReview.Staged)) ||
+		(safeOutputs.ReplyToPullRequestReviewComment != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.ReplyToPullRequestReviewComment.Staged)) ||
+		(safeOutputs.ResolvePullRequestReviewThread != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.ResolvePullRequestReviewThread.Staged)) {
 		safeOutputsPermissionsLog.Print("Adding permissions for PR review operations")
 		permissions.Merge(NewPermissionsContentsReadPRWrite())
 	}
-	if safeOutputs.CreatePullRequests != nil {
+	if safeOutputs.CreatePullRequests != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CreatePullRequests.Staged) {
 		// Check fallback-as-issue setting to determine permissions
 		if getFallbackAsIssue(safeOutputs.CreatePullRequests) {
 			safeOutputsPermissionsLog.Print("Adding permissions for create-pull-request with fallback-as-issue")
@@ -118,23 +131,23 @@ func ComputePermissionsForSafeOutputs(safeOutputs *SafeOutputsConfig) *Permissio
 			permissions.Merge(NewPermissionsContentsWritePRWrite())
 		}
 	}
-	if safeOutputs.PushToPullRequestBranch != nil {
+	if safeOutputs.PushToPullRequestBranch != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.PushToPullRequestBranch.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for push-to-pull-request-branch")
 		permissions.Merge(NewPermissionsContentsWritePRWrite())
 	}
-	if safeOutputs.UpdatePullRequests != nil {
+	if safeOutputs.UpdatePullRequests != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.UpdatePullRequests.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for update-pull-request")
 		permissions.Merge(NewPermissionsContentsReadPRWrite())
 	}
-	if safeOutputs.ClosePullRequests != nil {
+	if safeOutputs.ClosePullRequests != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.ClosePullRequests.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for close-pull-request")
 		permissions.Merge(NewPermissionsContentsReadPRWrite())
 	}
-	if safeOutputs.MarkPullRequestAsReadyForReview != nil {
+	if safeOutputs.MarkPullRequestAsReadyForReview != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.MarkPullRequestAsReadyForReview.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for mark-pull-request-as-ready-for-review")
 		permissions.Merge(NewPermissionsContentsReadPRWrite())
 	}
-	if safeOutputs.HideComment != nil {
+	if safeOutputs.HideComment != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.HideComment.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for hide-comment")
 		// Check if discussions permission should be excluded (discussions: false)
 		// Default (nil or true) includes discussions:write for GitHub Apps with Discussions permission
@@ -145,60 +158,60 @@ func ComputePermissionsForSafeOutputs(safeOutputs *SafeOutputsConfig) *Permissio
 			permissions.Merge(NewPermissionsContentsReadIssuesWriteDiscussionsWrite())
 		}
 	}
-	if safeOutputs.DispatchWorkflow != nil {
+	if safeOutputs.DispatchWorkflow != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.DispatchWorkflow.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for dispatch-workflow")
 		permissions.Merge(NewPermissionsActionsWrite())
 	}
 	// Project-related types
-	if safeOutputs.CreateProjects != nil {
+	if safeOutputs.CreateProjects != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CreateProjects.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for create-project")
 		permissions.Merge(NewPermissionsContentsReadProjectsWrite())
 	}
-	if safeOutputs.UpdateProjects != nil {
+	if safeOutputs.UpdateProjects != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.UpdateProjects.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for update-project")
 		permissions.Merge(NewPermissionsContentsReadProjectsWrite())
 	}
-	if safeOutputs.CreateProjectStatusUpdates != nil {
+	if safeOutputs.CreateProjectStatusUpdates != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CreateProjectStatusUpdates.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for create-project-status-update")
 		permissions.Merge(NewPermissionsContentsReadProjectsWrite())
 	}
-	if safeOutputs.AssignToAgent != nil {
+	if safeOutputs.AssignToAgent != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.AssignToAgent.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for assign-to-agent")
 		permissions.Merge(NewPermissionsContentsReadIssuesWrite())
 	}
-	if safeOutputs.CreateAgentSessions != nil {
+	if safeOutputs.CreateAgentSessions != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CreateAgentSessions.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for create-agent-session")
 		permissions.Merge(NewPermissionsContentsReadIssuesWrite())
 	}
-	if safeOutputs.CreateCodeScanningAlerts != nil {
+	if safeOutputs.CreateCodeScanningAlerts != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CreateCodeScanningAlerts.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for create-code-scanning-alert")
 		permissions.Merge(NewPermissionsContentsReadSecurityEventsWrite())
 	}
-	if safeOutputs.AutofixCodeScanningAlert != nil {
+	if safeOutputs.AutofixCodeScanningAlert != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.AutofixCodeScanningAlert.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for autofix-code-scanning-alert")
 		permissions.Merge(NewPermissionsContentsReadSecurityEventsWriteActionsRead())
 	}
-	if safeOutputs.AssignToUser != nil {
+	if safeOutputs.AssignToUser != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.AssignToUser.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for assign-to-user")
 		permissions.Merge(NewPermissionsContentsReadIssuesWrite())
 	}
-	if safeOutputs.UnassignFromUser != nil {
+	if safeOutputs.UnassignFromUser != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.UnassignFromUser.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for unassign-from-user")
 		permissions.Merge(NewPermissionsContentsReadIssuesWrite())
 	}
-	if safeOutputs.AssignMilestone != nil {
+	if safeOutputs.AssignMilestone != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.AssignMilestone.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for assign-milestone")
 		permissions.Merge(NewPermissionsContentsReadIssuesWrite())
 	}
-	if safeOutputs.SetIssueType != nil {
+	if safeOutputs.SetIssueType != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.SetIssueType.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for set-issue-type")
 		permissions.Merge(NewPermissionsContentsReadIssuesWrite())
 	}
-	if safeOutputs.AddReviewer != nil {
+	if safeOutputs.AddReviewer != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.AddReviewer.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for add-reviewer")
 		permissions.Merge(NewPermissionsContentsReadPRWrite())
 	}
-	if safeOutputs.UploadAssets != nil {
+	if safeOutputs.UploadAssets != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.UploadAssets.Staged) {
 		safeOutputsPermissionsLog.Print("Adding permissions for upload-asset")
 		permissions.Merge(NewPermissionsContentsWrite())
 	}
diff --git a/pkg/workflow/safe_outputs_permissions_test.go b/pkg/workflow/safe_outputs_permissions_test.go
index 993542c0fbe..43d1e0a11c8 100644
--- a/pkg/workflow/safe_outputs_permissions_test.go
+++ b/pkg/workflow/safe_outputs_permissions_test.go
@@ -570,3 +570,126 @@ func TestComputePermissionsForSafeOutputs_IDToken(t *testing.T) {
 		})
 	}
 }
+
+func TestComputePermissionsForSafeOutputs_Staged(t *testing.T) {
+	tests := []struct {
+		name        string
+		safeOutputs *SafeOutputsConfig
+		expected    map[PermissionScope]PermissionLevel
+	}{
+		{
+			name: "global staged=true - no permissions for any handler",
+			safeOutputs: &SafeOutputsConfig{
+				Staged:            true,
+				CreateIssues:      &CreateIssuesConfig{},
+				CreateDiscussions: &CreateDiscussionsConfig{},
+				AddLabels:         &AddLabelsConfig{},
+			},
+			expected: map[PermissionScope]PermissionLevel{},
+		},
+		{
+			name: "per-handler staged=true - staged handler contributes no permissions",
+			safeOutputs: &SafeOutputsConfig{
+				CreateIssues: &CreateIssuesConfig{
+					BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true},
+				},
+				AddLabels: &AddLabelsConfig{},
+			},
+			// create-issue is staged so it contributes nothing; add-labels is not staged
+			expected: map[PermissionScope]PermissionLevel{
+				PermissionContents:     PermissionRead,
+				PermissionIssues:       PermissionWrite,
+				PermissionPullRequests: PermissionWrite,
+			},
+		},
+		{
+			name: "all handlers per-handler staged - no permissions",
+			safeOutputs: &SafeOutputsConfig{
+				CreateIssues: &CreateIssuesConfig{
+					BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true},
+				},
+				CreateDiscussions: &CreateDiscussionsConfig{
+					BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true},
+				},
+			},
+			expected: map[PermissionScope]PermissionLevel{},
+		},
+		{
+			name: "global staged=true overrides per-handler staged=false",
+			safeOutputs: &SafeOutputsConfig{
+				Staged: true,
+				CreatePullRequests: &CreatePullRequestsConfig{
+					BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: false},
+				},
+				DispatchWorkflow: &DispatchWorkflowConfig{},
+			},
+			expected: map[PermissionScope]PermissionLevel{},
+		},
+		{
+			name: "global staged=false, one handler staged=true",
+			safeOutputs: &SafeOutputsConfig{
+				Staged: false,
+				CreatePullRequests: &CreatePullRequestsConfig{
+					BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true},
+				},
+				CloseIssues: &CloseIssuesConfig{},
+			},
+			// create-pull-request is staged; close-issue is not
+			expected: map[PermissionScope]PermissionLevel{
+				PermissionContents: PermissionRead,
+				PermissionIssues:   PermissionWrite,
+			},
+		},
+		{
+			name: "global staged=true - upload-asset staged, no contents:write",
+			safeOutputs: &SafeOutputsConfig{
+				Staged:       true,
+				UploadAssets: &UploadAssetsConfig{},
+			},
+			expected: map[PermissionScope]PermissionLevel{},
+		},
+		{
+			name: "pr review operations - all staged via global flag",
+			safeOutputs: &SafeOutputsConfig{
+				Staged:                          true,
+				CreatePullRequestReviewComments: &CreatePullRequestReviewCommentsConfig{},
+				SubmitPullRequestReview:         &SubmitPullRequestReviewConfig{},
+			},
+			expected: map[PermissionScope]PermissionLevel{},
+		},
+		{
+			name: "pr review operations - one staged, one not",
+			safeOutputs: &SafeOutputsConfig{
+				CreatePullRequestReviewComments: &CreatePullRequestReviewCommentsConfig{
+					BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true},
+				},
+				SubmitPullRequestReview: &SubmitPullRequestReviewConfig{},
+			},
+			// submit-pull-request-review is not staged, so PR write permissions are added
+			expected: map[PermissionScope]PermissionLevel{
+				PermissionContents:     PermissionRead,
+				PermissionPullRequests: PermissionWrite,
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			permissions := ComputePermissionsForSafeOutputs(tt.safeOutputs)
+			require.NotNil(t, permissions, "Permissions should not be nil")
+
+			// Check that all expected permissions are present
+			for scope, expectedLevel := range tt.expected {
+				actualLevel, exists := permissions.Get(scope)
+				assert.True(t, exists, "Permission scope %s should exist", scope)
+				assert.Equal(t, expectedLevel, actualLevel, "Permission level for %s should match", scope)
+			}
+
+			// Check that no unexpected permissions are present
+			for scope := range permissions.permissions {
+				_, expected := tt.expected[scope]
+				assert.True(t, expected, "Unexpected permission scope: %s", scope)
+			}
+		})
+	}
+}

From 9efbae89aee832063fc8215f51421df7cedb146c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 20 Mar 2026 04:58:13 +0000
Subject: [PATCH 4/7] Add integration tests and workflow files for staged safe
 output permissions

- Add test-staged-permissions-global.md: workflow with global staged:true
- Add test-staged-permissions-per-handler.md: workflow with per-handler staged:true
- Add TestCompileStagedSafeOutputsPermissionsGlobal: verifies no write permissions
  when safe-outputs.staged=true globally
- Add TestCompileStagedSafeOutputsPermissionsPerHandler: verifies only non-staged
  handlers contribute write permissions
- Add TestCompileStagedSafeOutputsPermissionsAllHandlersStaged: verifies no write
  permissions when all handlers are individually staged

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/1afcbdcd-09a8-4d0b-b876-34a29e28d315
---
 .github/workflows/poem-bot.lock.yml           |  10 -
 .github/workflows/smoke-claude.lock.yml       |   4 +-
 pkg/cli/compile_integration_test.go           | 181 +++++++++++++++++-
 .../test-staged-permissions-global.md         |  22 +++
 .../test-staged-permissions-per-handler.md    |  22 +++
 5 files changed, 225 insertions(+), 14 deletions(-)
 create mode 100644 pkg/cli/workflows/test-staged-permissions-global.md
 create mode 100644 pkg/cli/workflows/test-staged-permissions-per-handler.md

diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml
index c474fb2489a..0409e6772f4 100644
--- a/.github/workflows/poem-bot.lock.yml
+++ b/.github/workflows/poem-bot.lock.yml
@@ -1294,11 +1294,6 @@ jobs:
       - upload_assets
     if: (always()) && ((needs.agent.result != 'skipped') || (needs.activation.outputs.lockdown_check_failed == 'true'))
     runs-on: ubuntu-slim
-    permissions:
-      contents: write
-      discussions: write
-      issues: write
-      pull-requests: write
     concurrency:
       group: "gh-aw-conclusion-poem-bot"
       cancel-in-progress: false
@@ -1492,11 +1487,6 @@ jobs:
       - agent
     if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true')
     runs-on: ubuntu-slim
-    permissions:
-      contents: write
-      discussions: write
-      issues: write
-      pull-requests: write
     timeout-minutes: 15
     env:
       GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/poem-bot"
diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml
index 782262dffcc..7d6fae0a70f 100644
--- a/.github/workflows/smoke-claude.lock.yml
+++ b/.github/workflows/smoke-claude.lock.yml
@@ -2368,7 +2368,7 @@ jobs:
     if: (always()) && ((needs.agent.result != 'skipped') || (needs.activation.outputs.lockdown_check_failed == 'true'))
     runs-on: ubuntu-slim
     permissions:
-      contents: write
+      contents: read
       discussions: write
       issues: write
       pull-requests: write
@@ -2536,7 +2536,7 @@ jobs:
     if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true')
     runs-on: ubuntu-slim
     permissions:
-      contents: write
+      contents: read
       discussions: write
       issues: write
       pull-requests: write
diff --git a/pkg/cli/compile_integration_test.go b/pkg/cli/compile_integration_test.go
index 659ea65eeac..2d447c8fb18 100644
--- a/pkg/cli/compile_integration_test.go
+++ b/pkg/cli/compile_integration_test.go
@@ -1036,8 +1036,185 @@ Verify staged safe-outputs with multiple handler types.
 	}
 }
 
-// TestCompileFromSubdirectoryCreatesActionsLockAtRoot tests that actions-lock.json
-// is created at the repository root when compiling from a subdirectory
+// TestCompileStagedSafeOutputsPermissionsGlobal verifies that when safe-outputs has
+// global staged: true, the compiled safe_outputs job has no job-level permissions block
+// (staged mode emits only preview output; no GitHub API writes are performed).
+func TestCompileStagedSafeOutputsPermissionsGlobal(t *testing.T) {
+	setup := setupIntegrationTest(t)
+	defer setup.cleanup()
+
+	testWorkflow := `---
+name: Staged Global Permissions
+on:
+  workflow_dispatch:
+permissions:
+  contents: read
+engine: copilot
+safe-outputs:
+  staged: true
+  create-issue:
+    title-prefix: "[staged] "
+    max: 1
+  add-labels:
+    max: 3
+  create-discussion:
+    max: 1
+---
+
+Verify that global staged mode removes all write permissions from the safe_outputs job.
+`
+	testWorkflowPath := filepath.Join(setup.workflowsDir, "staged-global-perms.md")
+	if err := os.WriteFile(testWorkflowPath, []byte(testWorkflow), 0644); err != nil {
+		t.Fatalf("Failed to write test workflow file: %v", err)
+	}
+
+	cmd := exec.Command(setup.binaryPath, "compile", testWorkflowPath)
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		t.Fatalf("CLI compile command failed: %v\nOutput: %s", err, string(output))
+	}
+
+	lockFilePath := filepath.Join(setup.workflowsDir, "staged-global-perms.lock.yml")
+	lockContent, err := os.ReadFile(lockFilePath)
+	if err != nil {
+		t.Fatalf("Failed to read lock file: %v", err)
+	}
+	lockContentStr := string(lockContent)
+
+	// Global staged means no write API calls are made, so the safe_outputs job must
+	// have no job-level permissions block (permissions come from the workflow level).
+	if strings.Contains(lockContentStr, "issues: write") {
+		t.Errorf("Staged lock file should NOT contain 'issues: write' in safe_outputs job\nLock file content:\n%s", lockContentStr)
+	}
+	if strings.Contains(lockContentStr, "discussions: write") {
+		t.Errorf("Staged lock file should NOT contain 'discussions: write' in safe_outputs job\nLock file content:\n%s", lockContentStr)
+	}
+	if strings.Contains(lockContentStr, "pull-requests: write") {
+		t.Errorf("Staged lock file should NOT contain 'pull-requests: write' in safe_outputs job\nLock file content:\n%s", lockContentStr)
+	}
+	if strings.Contains(lockContentStr, "contents: write") {
+		t.Errorf("Staged lock file should NOT contain 'contents: write' in safe_outputs job\nLock file content:\n%s", lockContentStr)
+	}
+
+	// Staged env var must still be present
+	if !strings.Contains(lockContentStr, `GH_AW_SAFE_OUTPUTS_STAGED: "true"`) {
+		t.Errorf("Lock file should contain GH_AW_SAFE_OUTPUTS_STAGED: \"true\"\nLock file content:\n%s", lockContentStr)
+	}
+}
+
+// TestCompileStagedSafeOutputsPermissionsPerHandler verifies that when only specific
+// safe-output handlers have staged: true, only those handlers' write permissions are
+// omitted. Non-staged handlers still contribute their required permissions.
+func TestCompileStagedSafeOutputsPermissionsPerHandler(t *testing.T) {
+	setup := setupIntegrationTest(t)
+	defer setup.cleanup()
+
+	testWorkflow := `---
+name: Staged Per-Handler Permissions
+on:
+  workflow_dispatch:
+permissions:
+  contents: read
+engine: copilot
+safe-outputs:
+  create-issue:
+    staged: true
+    title-prefix: "[staged] "
+    max: 1
+  add-labels:
+    max: 3
+---
+
+Verify that per-handler staged mode removes only that handler's write permissions.
+`
+	testWorkflowPath := filepath.Join(setup.workflowsDir, "staged-perhandler-perms.md")
+	if err := os.WriteFile(testWorkflowPath, []byte(testWorkflow), 0644); err != nil {
+		t.Fatalf("Failed to write test workflow file: %v", err)
+	}
+
+	cmd := exec.Command(setup.binaryPath, "compile", testWorkflowPath)
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		t.Fatalf("CLI compile command failed: %v\nOutput: %s", err, string(output))
+	}
+
+	lockFilePath := filepath.Join(setup.workflowsDir, "staged-perhandler-perms.lock.yml")
+	lockContent, err := os.ReadFile(lockFilePath)
+	if err != nil {
+		t.Fatalf("Failed to read lock file: %v", err)
+	}
+	lockContentStr := string(lockContent)
+
+	// add-labels is not staged and needs issues: write and pull-requests: write
+	if !strings.Contains(lockContentStr, "issues: write") {
+		t.Errorf("Lock file should contain 'issues: write' for non-staged add-labels\nLock file content:\n%s", lockContentStr)
+	}
+	if !strings.Contains(lockContentStr, "pull-requests: write") {
+		t.Errorf("Lock file should contain 'pull-requests: write' for non-staged add-labels\nLock file content:\n%s", lockContentStr)
+	}
+
+	// create-issue is staged so it must NOT add issues: write on its own.
+	// However add-labels already contributes issues: write, so we can only verify
+	// that discussions and contents: write are absent (which create-issue does not add
+	// anyway). The key behaviour is verified via the unit tests in safe_outputs_permissions_test.go.
+	if strings.Contains(lockContentStr, "discussions: write") {
+		t.Errorf("Lock file should NOT contain 'discussions: write' when only add-labels and staged create-issue are configured\nLock file content:\n%s", lockContentStr)
+	}
+	if strings.Contains(lockContentStr, "contents: write") {
+		t.Errorf("Lock file should NOT contain 'contents: write'\nLock file content:\n%s", lockContentStr)
+	}
+}
+
+// TestCompileStagedSafeOutputsPermissionsAllHandlersStaged verifies that when all
+// handlers are per-handler staged, the safe_outputs job has no write permissions.
+func TestCompileStagedSafeOutputsPermissionsAllHandlersStaged(t *testing.T) {
+	setup := setupIntegrationTest(t)
+	defer setup.cleanup()
+
+	testWorkflow := `---
+name: All Handlers Staged
+on:
+  workflow_dispatch:
+permissions:
+  contents: read
+engine: copilot
+safe-outputs:
+  create-issue:
+    staged: true
+    max: 1
+  create-discussion:
+    staged: true
+    max: 1
+---
+
+Verify that when all handlers are per-handler staged, no write permissions appear.
+`
+	testWorkflowPath := filepath.Join(setup.workflowsDir, "staged-all-handlers.md")
+	if err := os.WriteFile(testWorkflowPath, []byte(testWorkflow), 0644); err != nil {
+		t.Fatalf("Failed to write test workflow file: %v", err)
+	}
+
+	cmd := exec.Command(setup.binaryPath, "compile", testWorkflowPath)
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		t.Fatalf("CLI compile command failed: %v\nOutput: %s", err, string(output))
+	}
+
+	lockFilePath := filepath.Join(setup.workflowsDir, "staged-all-handlers.lock.yml")
+	lockContent, err := os.ReadFile(lockFilePath)
+	if err != nil {
+		t.Fatalf("Failed to read lock file: %v", err)
+	}
+	lockContentStr := string(lockContent)
+
+	// All handlers are staged — no write permissions should appear in safe_outputs job
+	for _, perm := range []string{"issues: write", "discussions: write", "pull-requests: write", "contents: write"} {
+		if strings.Contains(lockContentStr, perm) {
+			t.Errorf("Staged lock file should NOT contain %q\nLock file content:\n%s", perm, lockContentStr)
+		}
+	}
+}
+
 func TestCompileFromSubdirectoryCreatesActionsLockAtRoot(t *testing.T) {
 	setup := setupIntegrationTest(t)
 	defer setup.cleanup()
diff --git a/pkg/cli/workflows/test-staged-permissions-global.md b/pkg/cli/workflows/test-staged-permissions-global.md
new file mode 100644
index 00000000000..b1d8a5aa1a6
--- /dev/null
+++ b/pkg/cli/workflows/test-staged-permissions-global.md
@@ -0,0 +1,22 @@
+---
+on:
+  workflow_dispatch:
+permissions:
+  contents: read
+engine: copilot
+safe-outputs:
+  staged: true
+  create-issue:
+    title-prefix: "[staged] "
+    max: 1
+  add-labels:
+    max: 3
+  create-discussion:
+    max: 1
+---
+
+# Test Staged Permissions (Global)
+
+Verify that when `staged: true` is set globally, the compiled safe_outputs job
+has **no** job-level `permissions:` block (all handlers are staged, so no write
+permissions are needed).
diff --git a/pkg/cli/workflows/test-staged-permissions-per-handler.md b/pkg/cli/workflows/test-staged-permissions-per-handler.md
new file mode 100644
index 00000000000..4f3421116a6
--- /dev/null
+++ b/pkg/cli/workflows/test-staged-permissions-per-handler.md
@@ -0,0 +1,22 @@
+---
+on:
+  workflow_dispatch:
+permissions:
+  contents: read
+engine: copilot
+safe-outputs:
+  create-issue:
+    staged: true
+    title-prefix: "[staged] "
+    max: 1
+  add-labels:
+    max: 3
+---
+
+# Test Staged Permissions (Per-Handler)
+
+Verify that when only specific handlers have `staged: true`, the compiled
+safe_outputs job only includes permissions required by the non-staged handlers.
+
+Here `create-issue` is staged (no write permissions for it), and `add-labels`
+is not staged (needs `issues: write` and `pull-requests: write`).

From eadce7b2c27dc99843758a9779e3334b653b92b5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 20 Mar 2026 05:09:12 +0000
Subject: [PATCH 5/7] Fix missing trailing slashes in docs internal links

22 links across blog posts, guides, and setup pages were missing
trailing slashes, causing the Starlight link validator to fail the
Doc Build CI job. Add trailing slashes to:
- /gh-aw/setup/creating-workflows -> /gh-aw/setup/creating-workflows/
- /gh-aw/guides/agentic-authoring -> /gh-aw/guides/agentic-authoring/
- /gh-aw/agent-factory-status -> /gh-aw/agent-factory-status/

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/79f8e4d7-5e14-498f-9e1e-3d0fbfd1b3ea
---
 .../docs/blog/2026-01-12-welcome-to-pelis-agent-factory.md      | 2 +-
 .../blog/2026-01-13-meet-the-workflows-advanced-analytics.md    | 2 +-
 .../docs/blog/2026-01-13-meet-the-workflows-campaigns.md        | 2 +-
 .../2026-01-13-meet-the-workflows-continuous-improvement.md     | 2 +-
 .../2026-01-13-meet-the-workflows-continuous-refactoring.md     | 2 +-
 .../blog/2026-01-13-meet-the-workflows-continuous-simplicity.md | 2 +-
 .../docs/blog/2026-01-13-meet-the-workflows-continuous-style.md | 2 +-
 .../docs/blog/2026-01-13-meet-the-workflows-creative-culture.md | 2 +-
 .../docs/blog/2026-01-13-meet-the-workflows-documentation.md    | 2 +-
 .../blog/2026-01-13-meet-the-workflows-interactive-chatops.md   | 2 +-
 .../docs/blog/2026-01-13-meet-the-workflows-issue-management.md | 2 +-
 .../blog/2026-01-13-meet-the-workflows-metrics-analytics.md     | 2 +-
 .../docs/blog/2026-01-13-meet-the-workflows-multi-phase.md      | 2 +-
 .../blog/2026-01-13-meet-the-workflows-operations-release.md    | 2 +-
 .../docs/blog/2026-01-13-meet-the-workflows-organization.md     | 2 +-
 .../docs/blog/2026-01-13-meet-the-workflows-quality-hygiene.md  | 2 +-
 .../blog/2026-01-13-meet-the-workflows-security-compliance.md   | 2 +-
 .../blog/2026-01-13-meet-the-workflows-testing-validation.md    | 2 +-
 .../blog/2026-01-13-meet-the-workflows-tool-infrastructure.md   | 2 +-
 docs/src/content/docs/blog/2026-01-13-meet-the-workflows.md     | 2 +-
 docs/src/content/docs/guides/agentic-authoring.mdx              | 2 +-
 docs/src/content/docs/setup/creating-workflows.mdx              | 2 +-
 22 files changed, 22 insertions(+), 22 deletions(-)

diff --git a/docs/src/content/docs/blog/2026-01-12-welcome-to-pelis-agent-factory.md b/docs/src/content/docs/blog/2026-01-12-welcome-to-pelis-agent-factory.md
index d9885be43be..0d001721986 100644
--- a/docs/src/content/docs/blog/2026-01-12-welcome-to-pelis-agent-factory.md
+++ b/docs/src/content/docs/blog/2026-01-12-welcome-to-pelis-agent-factory.md
@@ -115,4 +115,4 @@ Want to start with automated agentic workflows on GitHub? See our [Quick Start](
 
 ## Factory Status
 
-[Current Factory Status](/gh-aw/agent-factory-status)
+[Current Factory Status](/gh-aw/agent-factory-status/)
diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-advanced-analytics.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-advanced-analytics.md
index 8f3d2ea0f46..0a221bdd2a8 100644
--- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-advanced-analytics.md
+++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-advanced-analytics.md
@@ -73,7 +73,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/
 
 Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions.
 
-You can also [create your own workflows](/gh-aw/setup/creating-workflows).
+You can also [create your own workflows](/gh-aw/setup/creating-workflows/).
 
 ## Learn More
 
diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-campaigns.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-campaigns.md
index 667aacf7ea3..89411a28b9f 100644
--- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-campaigns.md
+++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-campaigns.md
@@ -57,7 +57,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/
 
 Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions.
 
-You can also [create your own workflows](/gh-aw/setup/creating-workflows).
+You can also [create your own workflows](/gh-aw/setup/creating-workflows/).
 
 ## Learn More
 
diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-improvement.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-improvement.md
index d0622b07672..7aacfcc833f 100644
--- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-improvement.md
+++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-improvement.md
@@ -142,7 +142,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/
 
 Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions.
 
-You can also [create your own workflows](/gh-aw/setup/creating-workflows).
+You can also [create your own workflows](/gh-aw/setup/creating-workflows/).
 
 ## Next Up: Continuous Documentation
 
diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-refactoring.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-refactoring.md
index da456a72650..38bea6ad960 100644
--- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-refactoring.md
+++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-refactoring.md
@@ -80,7 +80,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/
 
 Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions.
 
-You can also [create your own workflows](/gh-aw/setup/creating-workflows).
+You can also [create your own workflows](/gh-aw/setup/creating-workflows/).
 
 ## Next Up: Continuous Style
 
diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-simplicity.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-simplicity.md
index b6ce54fac3a..0b1a980fdb4 100644
--- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-simplicity.md
+++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-simplicity.md
@@ -76,7 +76,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/
 
 Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions.
 
-You can also [create your own workflows](/gh-aw/setup/creating-workflows).
+You can also [create your own workflows](/gh-aw/setup/creating-workflows/).
 
 ## Next Up: Continuous Refactoring
 
diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-style.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-style.md
index 3f956fd8b84..80ea1fa793c 100644
--- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-style.md
+++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-style.md
@@ -56,7 +56,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/
 
 Then edit and remix the workflow specification to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions.
 
-You can also [create your own workflows](/gh-aw/setup/creating-workflows).
+You can also [create your own workflows](/gh-aw/setup/creating-workflows/).
 
 ## Next Up: Continuous Improvement
 
diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-creative-culture.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-creative-culture.md
index e55119086c6..179451dc881 100644
--- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-creative-culture.md
+++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-creative-culture.md
@@ -86,7 +86,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/
 
 Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions.
 
-You can also [create your own workflows](/gh-aw/setup/creating-workflows).
+You can also [create your own workflows](/gh-aw/setup/creating-workflows/).
 
 ## Learn More
 
diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-documentation.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-documentation.md
index dc97f75dc0c..210a0857f3d 100644
--- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-documentation.md
+++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-documentation.md
@@ -100,7 +100,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/
 
 Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions.
 
-You can also [create your own workflows](/gh-aw/setup/creating-workflows).
+You can also [create your own workflows](/gh-aw/setup/creating-workflows/).
 
 ## Learn More
 
diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-interactive-chatops.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-interactive-chatops.md
index 0ecacd352d5..bdb41d08fb3 100644
--- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-interactive-chatops.md
+++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-interactive-chatops.md
@@ -66,7 +66,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/
 
 Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions.
 
-You can also [create your own workflows](/gh-aw/setup/creating-workflows).
+You can also [create your own workflows](/gh-aw/setup/creating-workflows/).
 
 ## Learn More
 
diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-issue-management.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-issue-management.md
index 8f2a461aef1..55b6bb7a8aa 100644
--- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-issue-management.md
+++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-issue-management.md
@@ -73,7 +73,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/
 
 Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions.
 
-You can also [create your own workflows](/gh-aw/setup/creating-workflows).
+You can also [create your own workflows](/gh-aw/setup/creating-workflows/).
 
 ## Learn More
 
diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-metrics-analytics.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-metrics-analytics.md
index 540c123db27..cbdf0caad26 100644
--- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-metrics-analytics.md
+++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-metrics-analytics.md
@@ -64,7 +64,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/
 
 Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions.
 
-You can also [create your own workflows](/gh-aw/setup/creating-workflows).
+You can also [create your own workflows](/gh-aw/setup/creating-workflows/).
 
 ## Learn More
 
diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-multi-phase.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-multi-phase.md
index a7e19b4d703..2365c864315 100644
--- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-multi-phase.md
+++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-multi-phase.md
@@ -74,7 +74,7 @@ gh aw add-wizard githubnext/agentics/workflows/pr-fix.md
 
 Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions.
 
-You can also [create your own workflows](/gh-aw/setup/creating-workflows).
+You can also [create your own workflows](/gh-aw/setup/creating-workflows/).
 
 ## Learn More
 
diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-operations-release.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-operations-release.md
index 59f5468abce..df68110ebaa 100644
--- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-operations-release.md
+++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-operations-release.md
@@ -47,7 +47,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/
 
 Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions.
 
-You can also [create your own workflows](/gh-aw/setup/creating-workflows).
+You can also [create your own workflows](/gh-aw/setup/creating-workflows/).
 
 ## Learn More
 
diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-organization.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-organization.md
index bbf564f3899..b9b6274eb79 100644
--- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-organization.md
+++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-organization.md
@@ -64,7 +64,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/
 
 Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions.
 
-You can also [create your own workflows](/gh-aw/setup/creating-workflows).
+You can also [create your own workflows](/gh-aw/setup/creating-workflows/).
 
 ## Learn More
 
diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-quality-hygiene.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-quality-hygiene.md
index 1bd2e622729..3d715577e25 100644
--- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-quality-hygiene.md
+++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-quality-hygiene.md
@@ -68,7 +68,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/
 
 Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions.
 
-You can also [create your own workflows](/gh-aw/setup/creating-workflows).
+You can also [create your own workflows](/gh-aw/setup/creating-workflows/).
 
 ## Learn More
 
diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-security-compliance.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-security-compliance.md
index 52a367dbddd..996d8e8f00f 100644
--- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-security-compliance.md
+++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-security-compliance.md
@@ -80,7 +80,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/
 
 Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions.
 
-You can also [create your own workflows](/gh-aw/setup/creating-workflows).
+You can also [create your own workflows](/gh-aw/setup/creating-workflows/).
 
 ## Learn More
 
diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-testing-validation.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-testing-validation.md
index ec36f856054..0ed9216a439 100644
--- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-testing-validation.md
+++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-testing-validation.md
@@ -102,7 +102,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/
 
 Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions.
 
-You can also [create your own workflows](/gh-aw/setup/creating-workflows).
+You can also [create your own workflows](/gh-aw/setup/creating-workflows/).
 
 ## Learn More
 
diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-tool-infrastructure.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-tool-infrastructure.md
index 48c93ca5d97..ce935c0ac53 100644
--- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-tool-infrastructure.md
+++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-tool-infrastructure.md
@@ -66,7 +66,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/
 
 Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions.
 
-You can also [create your own workflows](/gh-aw/setup/creating-workflows).
+You can also [create your own workflows](/gh-aw/setup/creating-workflows/).
 
 ## Learn More
 
diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows.md
index c053e1a0a26..ebdc4a12192 100644
--- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows.md
+++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows.md
@@ -92,7 +92,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/
 
 Then edit and remix the workflow specification to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions.
 
-You can also [create your own workflows](/gh-aw/setup/creating-workflows).
+You can also [create your own workflows](/gh-aw/setup/creating-workflows/).
 
 ## Next Up: Code Quality & Refactoring Workflows
 
diff --git a/docs/src/content/docs/guides/agentic-authoring.mdx b/docs/src/content/docs/guides/agentic-authoring.mdx
index 78c513f5989..cfdde6054a8 100644
--- a/docs/src/content/docs/guides/agentic-authoring.mdx
+++ b/docs/src/content/docs/guides/agentic-authoring.mdx
@@ -8,7 +8,7 @@ import CopyEntireFileButton from '../../../components/CopyEntireFileButton.astro
 import Video from '../../../components/Video.astro';
 
 Using our authoring agent is an effective way to create, debug, optimize your agentic workflows.
-This is a continuation of the [Create Agentic Workflows](/gh-aw/setup/creating-workflows) page.
+This is a continuation of the [Create Agentic Workflows](/gh-aw/setup/creating-workflows/) page.
 
 ## Configuring Your Repository
 
diff --git a/docs/src/content/docs/setup/creating-workflows.mdx b/docs/src/content/docs/setup/creating-workflows.mdx
index 16f40cb7d86..9e292eef22d 100644
--- a/docs/src/content/docs/setup/creating-workflows.mdx
+++ b/docs/src/content/docs/setup/creating-workflows.mdx
@@ -148,4 +148,4 @@ To add a workflow from another repository, see [Reusing Workflows](/gh-aw/guides
 
 ## Learn More About Agentic Authoring
 
-The [Agentic Authoring](/gh-aw/guides/agentic-authoring) contains additional techniques to leverage agents to help build better agentic workflows.
\ No newline at end of file
+The [Agentic Authoring](/gh-aw/guides/agentic-authoring/) contains additional techniques to leverage agents to help build better agentic workflows.
\ No newline at end of file

From f799c0cc2c4f98eff8ffe21108a02426c305e89c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 20 Mar 2026 05:44:40 +0000
Subject: [PATCH 6/7] Add explicit permissions: {} for staged safe output
 handlers

- usesPatchesAndCheckouts: respect staged status for create-pull-request
  and push-to-pull-request-branch, returning false when all PR handlers
  are staged (no checkout/git steps added for staged-only configs)
- ComputePermissionsForSafeOutputs: return NewPermissionsEmpty() when
  safe_outputs is configured but all handlers are staged, so the compiled
  safe_outputs job renders "permissions: {}" explicitly
- Tests: add staged cases to TestUsesPatchesAndCheckouts, add YAML
  rendering assertions to TestComputePermissionsForSafeOutputs_Staged,
  add TestComputePermissionsForSafeOutputs_StagedYAMLRendering
- Recompile: poem-bot.lock.yml safe_outputs job now has permissions: {}

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/d9d7cf59-7cee-4a99-8c2d-a04ad9fa45a9
---
 .github/workflows/daily-choice-test.lock.yml  |  2 +
 .../workflows/notion-issue-summary.lock.yml   |  2 +
 .github/workflows/poem-bot.lock.yml           | 36 +-----------
 .../workflows/smoke-call-workflow.lock.yml    |  2 +
 .github/workflows/smoke-claude.lock.yml       | 34 +----------
 .github/workflows/smoke-codex.lock.yml        | 16 ++----
 .../safe_outputs_config_helpers_test.go       | 56 +++++++++++++++++++
 pkg/workflow/safe_outputs_permissions.go      |  9 +++
 pkg/workflow/safe_outputs_permissions_test.go | 46 +++++++++++++++
 pkg/workflow/safe_outputs_runtime.go          | 14 +++--
 10 files changed, 135 insertions(+), 82 deletions(-)

diff --git a/.github/workflows/daily-choice-test.lock.yml b/.github/workflows/daily-choice-test.lock.yml
index 2bef28cf969..6cc82db0d3e 100644
--- a/.github/workflows/daily-choice-test.lock.yml
+++ b/.github/workflows/daily-choice-test.lock.yml
@@ -909,6 +909,7 @@ jobs:
       - test_environment
     if: (always()) && ((needs.agent.result != 'skipped') || (needs.activation.outputs.lockdown_check_failed == 'true'))
     runs-on: ubuntu-slim
+    permissions: {}
     concurrency:
       group: "gh-aw-conclusion-daily-choice-test"
       cancel-in-progress: false
@@ -1017,6 +1018,7 @@ jobs:
     needs: agent
     if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true')
     runs-on: ubuntu-slim
+    permissions: {}
     timeout-minutes: 15
     env:
       GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/daily-choice-test"
diff --git a/.github/workflows/notion-issue-summary.lock.yml b/.github/workflows/notion-issue-summary.lock.yml
index 91ba0ca85d0..bdce432d71c 100644
--- a/.github/workflows/notion-issue-summary.lock.yml
+++ b/.github/workflows/notion-issue-summary.lock.yml
@@ -748,6 +748,7 @@ jobs:
       - safe_outputs
     if: (always()) && ((needs.agent.result != 'skipped') || (needs.activation.outputs.lockdown_check_failed == 'true'))
     runs-on: ubuntu-slim
+    permissions: {}
     concurrency:
       group: "gh-aw-conclusion-notion-issue-summary"
       cancel-in-progress: false
@@ -981,6 +982,7 @@ jobs:
     needs: agent
     if: (!cancelled()) && (needs.agent.result != 'skipped')
     runs-on: ubuntu-slim
+    permissions: {}
     timeout-minutes: 15
     env:
       GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/notion-issue-summary"
diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml
index 0409e6772f4..cf2746e8659 100644
--- a/.github/workflows/poem-bot.lock.yml
+++ b/.github/workflows/poem-bot.lock.yml
@@ -1158,7 +1158,6 @@ jobs:
             /tmp/gh-aw/agent/
             /tmp/gh-aw/safeoutputs.jsonl
             /tmp/gh-aw/agent_output.json
-            /tmp/gh-aw/aw-*.patch
           if-no-files-found: ignore
       # --- Threat Detection (inline) ---
       - name: Check if detection needed
@@ -1294,6 +1293,7 @@ jobs:
       - upload_assets
     if: (always()) && ((needs.agent.result != 'skipped') || (needs.activation.outputs.lockdown_check_failed == 'true'))
     runs-on: ubuntu-slim
+    permissions: {}
     concurrency:
       group: "gh-aw-conclusion-poem-bot"
       cancel-in-progress: false
@@ -1482,11 +1482,10 @@ jobs:
             await main();
 
   safe_outputs:
-    needs:
-      - activation
-      - agent
+    needs: agent
     if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true')
     runs-on: ubuntu-slim
+    permissions: {}
     timeout-minutes: 15
     env:
       GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/poem-bot"
@@ -1538,34 +1537,6 @@ jobs:
           mkdir -p /tmp/gh-aw/
           find "/tmp/gh-aw/" -type f -print
           echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_ENV"
-      - name: Download patch artifact
-        continue-on-error: true
-        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
-        with:
-          name: agent
-          path: /tmp/gh-aw/
-      - name: Checkout repository
-        if: (((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request'))) || (((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch')))
-        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
-        with:
-          ref: ${{ github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }}
-          token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
-          persist-credentials: false
-          fetch-depth: 1
-      - name: Configure Git credentials
-        if: (((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request'))) || (((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch')))
-        env:
-          REPO_NAME: ${{ github.repository }}
-          SERVER_URL: ${{ github.server_url }}
-          GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
-        run: |
-          git config --global user.email "github-actions[bot]@users.noreply.github.com"
-          git config --global user.name "github-actions[bot]"
-          git config --global am.keepcr true
-          # Re-authenticate git with GitHub token
-          SERVER_URL_STRIPPED="${SERVER_URL#https://}"
-          git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
-          echo "Git configured with standard GitHub Actions identity"
       - name: Configure GH_HOST for enterprise compatibility
         shell: bash
         run: |
@@ -1584,7 +1555,6 @@ jobs:
           GITHUB_API_URL: ${{ github.api_url }}
           GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":3,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"poetry\",\"creative\",\"automation\",\"ai-generated\",\"epic\",\"haiku\",\"sonnet\",\"limerick\"],\"max\":5},\"close_pull_request\":{\"max\":2,\"required_labels\":[\"poetry\",\"automation\"],\"required_title_prefix\":\"[🎨 POETRY]\",\"target\":\"*\"},\"create_agent_session\":{\"base\":\"main\",\"max\":1},\"create_discussion\":{\"category\":\"audits\",\"close_older_discussions\":true,\"expires\":24,\"fallback_to_issue\":true,\"labels\":[\"poetry\",\"automation\",\"ai-generated\"],\"max\":2,\"title_prefix\":\"[📜 POETRY] \"},\"create_issue\":{\"expires\":48,\"group\":true,\"labels\":[\"poetry\",\"automation\",\"ai-generated\"],\"max\":2,\"title_prefix\":\"[🎭 POEM-BOT] \"},\"create_pull_request\":{\"draft\":false,\"expires\":48,\"labels\":[\"poetry\",\"automation\",\"creative-writing\"],\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"reviewers\":[\"copilot\"],\"title_prefix\":\"[🎨 POETRY] \"},\"create_pull_request_review_comment\":{\"max\":2,\"side\":\"RIGHT\"},\"link_sub_issue\":{\"max\":3,\"parent_required_labels\":[\"poetry\",\"epic\"],\"parent_title_prefix\":\"[🎭 POEM-BOT]\",\"sub_required_labels\":[\"poetry\"],\"sub_title_prefix\":\"[🎭 POEM-BOT]\"},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\"]},\"update_issue\":{\"allow_body\":true,\"allow_status\":true,\"allow_title\":true,\"max\":2,\"target\":\"*\"},\"upload_asset\":{\"allowed-exts\":[\".png\",\".jpg\",\".jpeg\"],\"branch\":\"assets/${{ github.workflow }}\",\"max-size\":10240}}"
           GH_AW_SAFE_OUTPUTS_STAGED: "true"
-          GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }}
         with:
           github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
           script: |
diff --git a/.github/workflows/smoke-call-workflow.lock.yml b/.github/workflows/smoke-call-workflow.lock.yml
index d6a8b43ebc2..35a462c19c9 100644
--- a/.github/workflows/smoke-call-workflow.lock.yml
+++ b/.github/workflows/smoke-call-workflow.lock.yml
@@ -865,6 +865,7 @@ jobs:
       - safe_outputs
     if: (always()) && ((needs.agent.result != 'skipped') || (needs.activation.outputs.lockdown_check_failed == 'true'))
     runs-on: ubuntu-slim
+    permissions: {}
     concurrency:
       group: "gh-aw-conclusion-smoke-call-workflow"
       cancel-in-progress: false
@@ -1004,6 +1005,7 @@ jobs:
     needs: agent
     if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true')
     runs-on: ubuntu-slim
+    permissions: {}
     timeout-minutes: 15
     env:
       GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/smoke-call-workflow"
diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml
index 7d6fae0a70f..74fb9203afc 100644
--- a/.github/workflows/smoke-claude.lock.yml
+++ b/.github/workflows/smoke-claude.lock.yml
@@ -2221,7 +2221,6 @@ jobs:
             /tmp/gh-aw/agent/
             /tmp/gh-aw/safeoutputs.jsonl
             /tmp/gh-aw/agent_output.json
-            /tmp/gh-aw/aw-*.patch
           if-no-files-found: ignore
       # --- Threat Detection (inline) ---
       - name: Check if detection needed
@@ -2530,9 +2529,7 @@ jobs:
             await main();
 
   safe_outputs:
-    needs:
-      - activation
-      - agent
+    needs: agent
     if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true')
     runs-on: ubuntu-slim
     permissions:
@@ -2586,34 +2583,6 @@ jobs:
           mkdir -p /tmp/gh-aw/
           find "/tmp/gh-aw/" -type f -print
           echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_ENV"
-      - name: Download patch artifact
-        continue-on-error: true
-        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
-        with:
-          name: agent
-          path: /tmp/gh-aw/
-      - name: Checkout repository
-        if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch'))
-        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
-        with:
-          ref: ${{ github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }}
-          token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
-          persist-credentials: false
-          fetch-depth: 1
-      - name: Configure Git credentials
-        if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch'))
-        env:
-          REPO_NAME: ${{ github.repository }}
-          SERVER_URL: ${{ github.server_url }}
-          GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
-        run: |
-          git config --global user.email "github-actions[bot]@users.noreply.github.com"
-          git config --global user.name "github-actions[bot]"
-          git config --global am.keepcr true
-          # Re-authenticate git with GitHub token
-          SERVER_URL_STRIPPED="${SERVER_URL#https://}"
-          git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
-          echo "Git configured with standard GitHub Actions identity"
       - name: Configure GH_HOST for enterprise compatibility
         shell: bash
         run: |
@@ -2655,7 +2624,6 @@ jobs:
           GITHUB_API_URL: ${{ github.api_url }}
           GH_AW_SAFE_OUTPUT_SCRIPTS: "{\"post_slack_message\":\"safe_output_script_post_slack_message.cjs\"}"
           GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-claude\"]},\"add_reviewer\":{\"max\":2,\"target\":\"*\"},\"close_pull_request\":{\"max\":1,\"staged\":true},\"create_issue\":{\"close_older_issues\":true,\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\",\"target\":\"*\"},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CLAUDE.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\",\".claude/\"],\"staged\":true,\"target\":\"*\"},\"resolve_pull_request_review_thread\":{\"max\":5},\"submit_pull_request_review\":{\"footer\":\"always\",\"max\":1},\"update_pull_request\":{\"allow_body\":true,\"allow_title\":true,\"max\":1,\"target\":\"*\"}}"
-          GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }}
         with:
           github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
           script: |
diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml
index a3c38f8c672..7514dfdddff 100644
--- a/.github/workflows/smoke-codex.lock.yml
+++ b/.github/workflows/smoke-codex.lock.yml
@@ -450,20 +450,13 @@ jobs:
               {
                 "description": "Add the 'smoked' label to the current pull request (can only be called once)",
                 "inputSchema": {
-                  "additionalProperties": false,
+                  "additionalProperties": true,
                   "properties": {
-                    "labels": {
-                      "description": "The labels' name to be added. Must be separated with line breaks if there're multiple labels.",
-                      "type": "string"
-                    },
-                    "number": {
-                      "description": "The number of the issue or pull request.",
+                    "payload": {
+                      "description": "JSON-encoded payload to pass to the action",
                       "type": "string"
                     }
                   },
-                  "required": [
-                    "labels"
-                  ],
                   "type": "object"
                 },
                 "name": "add_smoked_label"
@@ -1568,8 +1561,7 @@ jobs:
         env:
           GITHUB_TOKEN: ${{ github.token }}
         with:
-          labels: ${{ fromJSON(steps.process_safe_outputs.outputs.action_add_smoked_label_payload).labels }}
-          number: ${{ fromJSON(steps.process_safe_outputs.outputs.action_add_smoked_label_payload).number }}
+          payload: ${{ steps.process_safe_outputs.outputs.action_add_smoked_label_payload }}
       - name: Upload safe output items
         if: always()
         uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
diff --git a/pkg/workflow/safe_outputs_config_helpers_test.go b/pkg/workflow/safe_outputs_config_helpers_test.go
index 6c0700bbf17..8690a193a47 100644
--- a/pkg/workflow/safe_outputs_config_helpers_test.go
+++ b/pkg/workflow/safe_outputs_config_helpers_test.go
@@ -84,6 +84,62 @@ func TestUsesPatchesAndCheckouts(t *testing.T) {
 			},
 			expected: true,
 		},
+		{
+			name: "returns false when CreatePullRequests is globally staged",
+			safeOutputs: &SafeOutputsConfig{
+				Staged:             true,
+				CreatePullRequests: &CreatePullRequestsConfig{},
+			},
+			expected: false,
+		},
+		{
+			name: "returns false when PushToPullRequestBranch is globally staged",
+			safeOutputs: &SafeOutputsConfig{
+				Staged:                  true,
+				PushToPullRequestBranch: &PushToPullRequestBranchConfig{},
+			},
+			expected: false,
+		},
+		{
+			name: "returns false when both PR handlers are globally staged",
+			safeOutputs: &SafeOutputsConfig{
+				Staged:                  true,
+				CreatePullRequests:      &CreatePullRequestsConfig{},
+				PushToPullRequestBranch: &PushToPullRequestBranchConfig{},
+			},
+			expected: false,
+		},
+		{
+			name: "returns false when CreatePullRequests is per-handler staged",
+			safeOutputs: &SafeOutputsConfig{
+				CreatePullRequests: &CreatePullRequestsConfig{BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true}},
+			},
+			expected: false,
+		},
+		{
+			name: "returns false when both PR handlers are per-handler staged",
+			safeOutputs: &SafeOutputsConfig{
+				CreatePullRequests:      &CreatePullRequestsConfig{BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true}},
+				PushToPullRequestBranch: &PushToPullRequestBranchConfig{BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true}},
+			},
+			expected: false,
+		},
+		{
+			name: "returns true when CreatePullRequests is not staged but PushToPullRequestBranch is staged",
+			safeOutputs: &SafeOutputsConfig{
+				CreatePullRequests:      &CreatePullRequestsConfig{},
+				PushToPullRequestBranch: &PushToPullRequestBranchConfig{BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true}},
+			},
+			expected: true,
+		},
+		{
+			name: "returns true when PushToPullRequestBranch is not staged but CreatePullRequests is staged",
+			safeOutputs: &SafeOutputsConfig{
+				CreatePullRequests:      &CreatePullRequestsConfig{BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true}},
+				PushToPullRequestBranch: &PushToPullRequestBranchConfig{},
+			},
+			expected: true,
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/pkg/workflow/safe_outputs_permissions.go b/pkg/workflow/safe_outputs_permissions.go
index 4d46c710fa7..7df2a668b2a 100644
--- a/pkg/workflow/safe_outputs_permissions.go
+++ b/pkg/workflow/safe_outputs_permissions.go
@@ -232,6 +232,15 @@ func ComputePermissionsForSafeOutputs(safeOutputs *SafeOutputsConfig) *Permissio
 		permissions.Set(PermissionIdToken, PermissionWrite)
 	}
 
+	// If safeOutputs is configured but no permissions were accumulated (all handlers staged),
+	// return explicit empty permissions so the compiled safe_outputs job renders
+	// "permissions: {}" rather than omitting the block and inheriting workflow-level permissions.
+	// This makes the security posture self-documenting in the generated YAML.
+	if len(permissions.permissions) == 0 {
+		safeOutputsPermissionsLog.Print("All handlers staged; returning explicit empty permissions (permissions: {})")
+		return NewPermissionsEmpty()
+	}
+
 	safeOutputsPermissionsLog.Printf("Computed permissions with %d scopes", len(permissions.permissions))
 	return permissions
 }
diff --git a/pkg/workflow/safe_outputs_permissions_test.go b/pkg/workflow/safe_outputs_permissions_test.go
index 43d1e0a11c8..93ea41634c2 100644
--- a/pkg/workflow/safe_outputs_permissions_test.go
+++ b/pkg/workflow/safe_outputs_permissions_test.go
@@ -693,3 +693,49 @@ func TestComputePermissionsForSafeOutputs_Staged(t *testing.T) {
 		})
 	}
 }
+
+// TestComputePermissionsForSafeOutputs_StagedYAMLRendering validates that fully-staged
+// safe output configurations produce explicit "permissions: {}" in YAML rendering,
+// rather than an empty string that would cause the job to inherit workflow-level permissions.
+func TestComputePermissionsForSafeOutputs_StagedYAMLRendering(t *testing.T) {
+	tests := []struct {
+		name             string
+		safeOutputs      *SafeOutputsConfig
+		expectedRendered string
+	}{
+		{
+			name: "globally staged - renders permissions: {}",
+			safeOutputs: &SafeOutputsConfig{
+				Staged:       true,
+				CreateIssues: &CreateIssuesConfig{},
+				AddLabels:    &AddLabelsConfig{},
+			},
+			expectedRendered: "permissions: {}",
+		},
+		{
+			name: "all per-handler staged - renders permissions: {}",
+			safeOutputs: &SafeOutputsConfig{
+				CreateIssues: &CreateIssuesConfig{BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true}},
+				AddLabels:    &AddLabelsConfig{BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true}},
+			},
+			expectedRendered: "permissions: {}",
+		},
+		{
+			name: "staged PR handlers - renders permissions: {}",
+			safeOutputs: &SafeOutputsConfig{
+				Staged:             true,
+				CreatePullRequests: &CreatePullRequestsConfig{},
+			},
+			expectedRendered: "permissions: {}",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			permissions := ComputePermissionsForSafeOutputs(tt.safeOutputs)
+			require.NotNil(t, permissions, "Permissions should not be nil")
+			rendered := permissions.RenderToYAML()
+			assert.Equal(t, tt.expectedRendered, rendered, "Fully-staged safe-outputs must render explicit empty permissions block")
+		})
+	}
+}
diff --git a/pkg/workflow/safe_outputs_runtime.go b/pkg/workflow/safe_outputs_runtime.go
index 81da2659cfa..ce07fd36f06 100644
--- a/pkg/workflow/safe_outputs_runtime.go
+++ b/pkg/workflow/safe_outputs_runtime.go
@@ -28,13 +28,19 @@ func (c *Compiler) formatSafeOutputsRunsOn(safeOutputs *SafeOutputsConfig) strin
 }
 
 // usesPatchesAndCheckouts checks if the workflow uses safe outputs that require
-// git patches and checkouts (create-pull-request or push-to-pull-request-branch)
+// git patches and checkouts (create-pull-request or push-to-pull-request-branch).
+// Staged handlers are excluded because they only emit preview output and do not
+// perform real git operations or API calls.
 func usesPatchesAndCheckouts(safeOutputs *SafeOutputsConfig) bool {
 	if safeOutputs == nil {
 		return false
 	}
-	result := safeOutputs.CreatePullRequests != nil || safeOutputs.PushToPullRequestBranch != nil
-	safeOutputsRuntimeLog.Printf("usesPatchesAndCheckouts: createPR=%v, pushToPRBranch=%v, result=%v",
-		safeOutputs.CreatePullRequests != nil, safeOutputs.PushToPullRequestBranch != nil, result)
+	createPRNeedsCheckout := safeOutputs.CreatePullRequests != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CreatePullRequests.Staged)
+	pushToPRNeedsCheckout := safeOutputs.PushToPullRequestBranch != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.PushToPullRequestBranch.Staged)
+	result := createPRNeedsCheckout || pushToPRNeedsCheckout
+	safeOutputsRuntimeLog.Printf("usesPatchesAndCheckouts: createPR=%v(needsCheckout=%v), pushToPRBranch=%v(needsCheckout=%v), result=%v",
+		safeOutputs.CreatePullRequests != nil, createPRNeedsCheckout,
+		safeOutputs.PushToPullRequestBranch != nil, pushToPRNeedsCheckout,
+		result)
 	return result
 }

From 396bfd6ee1d9ab416abb6944a94e3ffac904acdb Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 20 Mar 2026 05:58:13 +0000
Subject: [PATCH 7/7] Add changeset [skip-ci]

---
 .../patch-skip-write-permissions-staged-safe-outputs.md      | 5 +++++
 1 file changed, 5 insertions(+)
 create mode 100644 .changeset/patch-skip-write-permissions-staged-safe-outputs.md

diff --git a/.changeset/patch-skip-write-permissions-staged-safe-outputs.md b/.changeset/patch-skip-write-permissions-staged-safe-outputs.md
new file mode 100644
index 00000000000..b8c59b5056f
--- /dev/null
+++ b/.changeset/patch-skip-write-permissions-staged-safe-outputs.md
@@ -0,0 +1,5 @@
+---
+"gh-aw": patch
+---
+
+Skip write permissions for staged safe output handlers so staged-only safe outputs compile with explicit empty permissions and avoid unnecessary checkout/setup steps.
