Repository: cli/cli
Pull request #11217 (2025-07-03 15:04:33)

Support `--no-repos-selected` on `gh secret set`
## Description

Fixes https://github.com/cli/cli/issues/9808

### Acceptance Criteria

*Given* there is no secret
*When* I run `gh secret set <secret_name> -b <secret_value> -o <org> -v selected --no-repos-selected`
*Then*  a secret called <secret_name> is created in <org> with a value of <secret_value> with the selected visibility and no repositories selected.

```
➜ gh secret list --org williammartin-test-org | cat

➜ gh secret set TEST -b BODY -o williammartin-test-org -v selected --no-repos-selected
✓ Set Actions secret TEST for williammartin-test-org

➜ gh api /orgs/williammartin-test-org/actions/secrets/TEST/repositories | cat
{"total_count":0,"repositories":[]}%
```

---

*Given* there is an existing secret with <secret_name>
*When* I run `gh secret set <secret_name> -b <secret_value> -o <org> -v selected --no-repos-selected`
*Then*  that secret is replaced with a value of <secret_value> with the selected visibility and no repositories selected.

```
➜ ./bin/gh secret set TEST -b BODY -o williammartin-test-org -v selected --repos test-repo                                                                                                                                                                 ✓ Set Actions secret TEST for williammartin-test-org

➜ gh api /orgs/williammartin-test-org/actions/secrets/TEST/repositories | cat
{"total_count":1,"repositories":[{"id":902814034,"node_id":"R_kgDONc_ZUg","name":"test-repo","full_name":"williammartin-test-org/test-repo","private":true,"owner":{"login":"williammartin-test-org","id":132901068,"node_id":"O_kgDOB-vozA","avatar_url":"https://avatars.githubusercontent.com/u/132901068?v=4","gravatar_id":"","url":"https://api.github.com/users/williammartin-test-org","html_url":"https://github.com/williammartin-test-org","followers_url":"https://api.github.com/users/williammartin-test-org/followers","following_url":"https://api.github.com/users/williammartin-test-org/following{/other_user}","gists_url":"https://api.github.com/users/williammartin-test-org/gists{/gist_id}","starred_url":"https://api.github.com/users/williammartin-test-org/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/williammartin-test-org/subscriptions","organizations_url":"https://api.github.com/users/williammartin-test-org/orgs","repos_url":"https://api.github.com/users/williammartin-test-org/repos","events_url":"https://api.github.com/users/williammartin-test-org/events{/privacy}","received_events_url":"https://api.github.com/users/williammartin-test-org/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/williammartin-test-org/test-repo","description":null,"fork":false,"url":"https://api.github.com/repos/williammartin-test-org/test-repo","forks_url":"https://api.github.com/repos/williammartin-test-org/test-repo/forks","keys_url":"https://api.github.com/repos/williammartin-test-org/test-repo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/williammartin-test-org/test-repo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/williammartin-test-org/test-repo/teams","hooks_url":"https://api.github.com/repos/williammartin-test-org/test-repo/hooks","issue_events_url":"https://api.github.com/repos/williammartin-test-org/test-repo/issues/events{/number}","events_url":"https://api.github.com/repos/williammartin-test-org/test-repo/events","assignees_url":"https://api.github.com/repos/williammartin-test-org/test-repo/assignees{/user}","branches_url":"https://api.github.com/repos/williammartin-test-org/test-repo/branches{/branch}","tags_url":"https://api.github.com/repos/williammartin-test-org/test-repo/tags","blobs_url":"https://api.github.com/repos/williammartin-test-org/test-repo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/williammartin-test-org/test-repo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/williammartin-test-org/test-repo/git/refs{/sha}","trees_url":"https://api.github.com/repos/williammartin-test-org/test-repo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/williammartin-test-org/test-repo/statuses/{sha}","languages_url":"https://api.github.com/repos/williammartin-test-org/test-repo/languages","stargazers_url":"https://api.github.com/repos/williammartin-test-org/test-repo/stargazers","contributors_url":"https://api.github.com/repos/williammartin-test-org/test-repo/contributors","subscribers_url":"https://api.github.com/repos/williammartin-test-org/test-repo/subscribers","subscription_url":"https://api.github.com/repos/williammartin-test-org/test-repo/subscription","commits_url":"https://api.github.com/repos/williammartin-test-org/test-repo/commits{/sha}","git_commits_url":"https://api.github.com/repos/williammartin-test-org/test-repo/git/commits{/sha}","comments_url":"https://api.github.com/repos/williammartin-test-org/test-repo/comments{/number}","issue_comment_url":"https://api.github.com/repos/williammartin-test-org/test-repo/issues/comments{/number}","contents_url":"https://api.github.com/repos/williammartin-test-org/test-repo/contents/{+path}","compare_url":"https://api.github.com/repos/williammartin-test-org/test-repo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/williammartin-test-org/test-repo/merges","archive_url":"https://api.github.com/repos/williammartin-test-org/test-repo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/williammartin-test-org/test-repo/downloads","issues_url":"https://api.github.com/repos/williammartin-test-org/test-repo/issues{/number}","pulls_url":"https://api.github.com/repos/williammartin-test-org/test-repo/pulls{/number}","milestones_url":"https://api.github.com/repos/williammartin-test-org/test-repo/milestones{/number}","notifications_url":"https://api.github.com/repos/williammartin-test-org/test-repo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/williammartin-test-org/test-repo/labels{/name}","releases_url":"https://api.github.com/repos/williammartin-test-org/test-repo/releases{/id}","deployments_url":"https://api.github.com/repos/williammartin-test-org/test-repo/deployments"}]}%

➜ ./bin/gh secret set TEST -b BODY -o williammartin-test-org -v selected --no-repos-selected
✓ Set Actions secret TEST for williammartin-test-org

➜ gh api /orgs/williammartin-test-org/actions/secrets/TEST/repositories | cat
{"total_count":0,"repositories":[]}%
```

---

*When* I run `gh secret set <secret_name> -b <secret_value> -o <org> -v selected --no-repos-selected --repos`
*Then*  then I receive an error message indicating the mutual exclusivity of the --no-repos-selected and --repos flags

```
➜ ./bin/gh secret set TEST -b BODY -o williammartin-test-org -v selected --no-repos-selected --repos cli/cli
specify only one of `--repos` or `--no-repos-selected`
```

---

*When* I run `gh secret set <secret_name> -b <secret_value> -o <org> --no-repos-selected`
*Then*  then --visibility selected is implied

```
➜ ./bin/gh secret set TEST -b BODY -o williammartin-test-org --no-repos-selected
✓ Set Actions secret TEST for williammartin-test-org

➜ gh api /orgs/williammartin-test-org/actions/secrets/TEST/repositories | cat                                                                                                             {"total_count":0,"repositories":[]}%
```

---

*When* I run `gh secret set <secret_name> -b <secret_value> -o <org> -v <private/all> --no-repos-selected`
*Then*  then I receive an error message indicating that --no-repos-selected may only be used with --visibility selected

```
➜ ./bin/gh secret set TEST -b BODY -o williammartin-test-org -v private --no-repos-selected
`--no-repos-selected` is only supported with `--visibility=selected`
```

---

*When* I run `gh secret set <secret_name> -b <secret_value> -o <org> -v selected`
*Then*  then I receive an error message indicating that providing either a list of repos with --repos or --no-repo-selected is required.

```
➜ ./bin/gh secret set TEST -b BODY -o williammartin-test-org -v selected
`--repos` or `--no-repos-selected` required with `--visibility=selected`
```

---

*When* I run `gh secret set --help`
*Then*  then there is a note about --no-repos-selected that indicates if there are any repos selected on an existing secret, then they will be cleared.A

```
➜ ./bin/gh secret set --help
Set a value for a secret on one of the following levels:
- repository (default): available to GitHub Actions runs or Dependabot in a repository
- environment: available to GitHub Actions runs for a deployment environment in a repository
- organization: available to GitHub Actions runs, Dependabot, or Codespaces within an organization
- user: available to Codespaces for your user

Organization and user secrets can optionally be restricted to only be available to
specific repositories.

Secret values are locally encrypted before being sent to GitHub.


USAGE
  gh secret set <secret-name> [flags]

FLAGS
  -a, --app string           Set the application for a secret: {actions|codespaces|dependabot}
  -b, --body string          The value for the secret (reads from standard input if not specified)
  -e, --env environment      Set deployment environment secret
  -f, --env-file file        Load secret names and values from a dotenv-formatted file
      --no-repos-selected    No repositories can access the organization or user secret
      --no-store             Print the encrypted, base64-encoded value instead of storing it on GitHub
  -o, --org organization     Set organization secret
  -r, --repos repositories   List of repositories that can access an organization secret
  -u, --user                 Set a secret for your user
  -v, --visibility string    Set visibility for an organization secret: {all|private|selected} (default "private")

INHERITED FLAGS
      --help                     Show help for command
  -R, --repo [HOST/]OWNER/REPO   Select another repository using the [HOST/]OWNER/REPO format

EXAMPLES
  # Paste secret value for the current repository in an interactive prompt
  $ gh secret set MYSECRET

  # Read secret value from an environment variable
  $ gh secret set MYSECRET --body "$ENV_VALUE"

  # Set secret for a specific remote repository
  $ gh secret set MYSECRET --repo origin/repo --body "$ENV_VALUE"

  # Read secret value from a file
  $ gh secret set MYSECRET < myfile.txt

  # Set secret for a deployment environment in the current repository
  $ gh secret set MYSECRET --env myenvironment

  # Set organization-level secret visible to both public and private repositories
  $ gh secret set MYSECRET --org myOrg --visibility all

  # Set organization-level secret visible to specific repositories
  $ gh secret set MYSECRET --org myOrg --repos repo1,repo2,repo3

  # Set organization-level secret visible to no repositories
  $ gh secret set MYSECRET --org myOrg --no-repos-selected

  # Set user-level secret for Codespaces
  $ gh secret set MYSECRET --user

  # Set repository-level secret for Dependabot
  $ gh secret set MYSECRET --app dependabot

  # Set multiple secrets imported from the ".env" file
  $ gh secret set -f .env

  # Set multiple secrets from stdin
  $ gh secret set -f - < myfile.txt

LEARN MORE
  Use `gh <command> <subcommand> --help` for more information about a command.
  Read the manual at https://cli.github.com/manual
  Learn about exit codes using `gh help exit-codes`
  Learn about accessibility experiences using `gh help accessibility`
```

### Acceptance Test

```
➜ GOMAXPROCS=1 GH_ACCEPTANCE_HOST=github.com GH_ACCEPTANCE_ORG=gh-acceptance-testing GH_ACCEPTANCE_SCRIPT=secret-org-with-selected-visibility.txtar go test -count=1 -tags=acceptance -run ^TestSecrets$ ./acceptance -v
=== RUN   TestSecrets
=== RUN   TestSecrets/secret-org-with-selected-visibility
=== PAUSE TestSecrets/secret-org-with-selected-visibility
=== CONT  TestSecrets/secret-org-with-selected-visibility
    testscript.go:584: WORK=$WORK
        PATH=/var/folders/45/sdnm1hp10nj1s9q57dp3bc5h0000gn/T/testscript-main1133164272/bin:/opt/homebrew/opt/go/libexec/bin:/Users/williammartin/.bun/bin:/Users/williammartin/.bun/bin:/Users/williammartin/.local/bin:/opt/homebrew/opt/go/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/usr/local/MacGPG2/bin:/Applications/Wireshark.app/Contents/MacOS:/Applications/VMware Fusion.app/Contents/Public:/usr/local/go/bin:/Users/williammartin/.cargo/bin:/Applications/Ghostty.app/Contents/MacOS:/Users/williammartin/go/bin/:/opt/homebrew/opt/go/libexec/bin
        GOTRACEBACK=system
        HOME=$WORK
        TMPDIR=$WORK/.tmp
        devnull=/dev/null
        /=/
        :=:
        $=$
        exe=
        SCRIPT_NAME=secret_org_with_selected_visibility
        GH_CONFIG_DIR=$WORK
        GH_HOST=github.com
        ORG=gh-acceptance-testing
        GH_TOKEN=ghp_************************************
        RANDOM_STRING=DWbDxBVpDI

        # Setup environment variables used for testscript (0.000s)
        > env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
        > env2upper SECRET_NAME=${SCRIPT_NAME}_${RANDOM_STRING}
        # Use gh as a credential helper (0.567s)
        > exec gh auth setup-git
        # Create a repository with a file so it has a default branch (1.702s)
        > exec gh repo create ${ORG}/${REPO} --add-readme --private
        [stdout]
        https://github.com/gh-acceptance-testing/secret_org_with_selected_visibility-DWbDxBVpDI
        # Defer repo cleanup (0.000s)
        > defer gh repo delete --yes ${ORG}/${REPO}
        # Confirm organization secret does not exist, will fail admin:org scope missing (0.330s)
        > exec gh secret list --org ${ORG}
        [stdout]
        SECRET_ORG_UZKSOQYGTO   2025-07-03T14:40:27Z    SELECTED
        SECRET_ORG_XYTTEDZKWF   2025-07-03T14:37:13Z    SELECTED
        > ! stdout ${SECRET_NAME}
        # Set an organization secret with no shared visibility, but no repos (0.529s)
        > exec gh secret set ${SECRET_NAME} --org ${ORG} --body 'just an organization secret' --no-repos-selected
        # Defer organization secret cleanup (0.000s)
        > defer gh secret delete ${SECRET_NAME} --org ${ORG}
        # Verify new organization secret exists with shared visibility (0.278s)
        > exec gh api -X GET /orgs/${ORG}/actions/secrets/${SECRET_NAME} --jq '.visibility'
        [stdout]
        selected
        > stdout selected
        # Verify the secret is not shared with any repositories (0.314s)
        > exec gh api -X GET /orgs/${ORG}/actions/secrets/${SECRET_NAME}/repositories --jq '.repositories | length'
        [stdout]
        0
        > stdout 0
        # Set the same organization secret with shared visibility to the previously created repository (0.963s)
        > exec gh secret set ${SECRET_NAME} --org ${ORG} --body 'just an organization secret' --repos ${REPO}
        # Verify the secret is now shared with the repository (0.313s)
        > exec gh api -X GET /orgs/${ORG}/actions/secrets/${SECRET_NAME}/repositories --jq '.repositories[0].name'
        [stdout]
        secret_org_with_selected_visibility-DWbDxBVpDI
        > stdout ${REPO}
        # Set the same organization secret with shared visibility back  to no repositories selected (0.560s)
        > exec gh secret set ${SECRET_NAME} --org ${ORG} --body 'just an organization secret' --no-repos-selected
        # Verify the secret is not shared with any repositories (0.304s)
        > exec gh api -X GET /orgs/${ORG}/actions/secrets/${SECRET_NAME}/repositories --jq '.repositories | length'
        [stdout]
        0
        > stdout 0
        PASS

--- PASS: TestSecrets (0.00s)
    --- PASS: TestSecrets/secret-org-with-selected-visibility (6.71s)
PASS
ok      github.com/cli/cli/v2/acceptance        7.153s
```



Patch:
diff --git a/pkg/cmd/secret/set/set.go b/pkg/cmd/secret/set/set.go
index 0a6581559..7fbc77552 100644
--- a/pkg/cmd/secret/set/set.go
+++ b/pkg/cmd/secret/set/set.go
@@ -53,6 +53,11 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
 		Prompter:   f.Prompter,
 	}
 
+	// It is possible for a user to say `--no-repos-selected=false --repos cli/cli` and that would be equivalent to not
+	// specifying the flag at all. We could avoid this by checking whether the flag was set at all, but it seems like
+	// more trouble than it's worth since anyone who does `--no-repos-selected=false` is gonna get what's coming to them.
+	var noRepositoriesSelected bool
+
 	cmd := &cobra.Command{
 		Use:   "set <secret-name>",
 		Short: "Create or update secrets",
@@ -90,6 +95,9 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
 			# Set organization-level secret visible to specific repositories
 			$ gh secret set MYSECRET --org myOrg --repos repo1,repo2,repo3
 
+			# Set organization-level secret visible to no repositories
+			$ gh secret set MYSECRET --org myOrg --no-repos-selected
+
 			# Set user-level secret for Codespaces
 			$ gh secret set MYSECRET --user
 
@@ -131,6 +139,14 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
 				return err
 			}
 
+			if err := cmdutil.MutuallyExclusive("specify only one of `--repos` or `--no-repos-selected`", len(opts.RepositoryNames) > 0, noRepositoriesSelected); err != nil {
+				return err
+			}
+
+			if err := cmdutil.MutuallyExclusive("`--no-repos-selected` must be omitted when used with `--user`", opts.UserSecrets, noRepositoriesSelected); err != nil {
+				return err
+			}
+
 			if len(args) == 0 {
 				if !opts.DoNotStore && opts.EnvFile == "" {
 					return cmdutil.FlagErrorf("must pass name argument")
@@ -148,11 +164,16 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
 					return cmdutil.FlagErrorf("`--repos` is only supported with `--visibility=selected`")
 				}
 
-				if opts.Visibility == shared.Selected && len(opts.RepositoryNames) == 0 {
-					return cmdutil.FlagErrorf("`--repos` list required with `--visibility=selected`")
+				if opts.Visibility != shared.Selected && noRepositoriesSelected {
+					return cmdutil.FlagErrorf("`--no-repos-selected` is only supported with `--visibility=selected`")
 				}
+
+				if opts.Visibility == shared.Selected && (len(opts.RepositoryNames) == 0 && !noRepositoriesSelected) {
+					return cmdutil.FlagErrorf("`--repos` or `--no-repos-selected` required with `--visibility=selected`")
+				}
+
 			} else {
-				if len(opts.RepositoryNames) > 0 {
+				if len(opts.RepositoryNames) > 0 || noRepositoriesSelected {
 					opts.Visibility = shared.Selected
 				}
 			}
@@ -170,6 +191,7 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
 	cmd.Flags().BoolVarP(&opts.UserSecrets, "user", "u", false, "Set a secret for your user")
 	cmdutil.StringEnumFlag(cmd, &opts.Visibility, "visibility", "v", shared.Private, []string{shared.All, shared.Private, shared.Selected}, "Set visibility for an organization secret")
 	cmd.Flags().StringSliceVarP(&opts.RepositoryNames, "repos", "r", []string{}, "List of `repositories` that can access an organization or user secret")
+	cmd.Flags().BoolVar(&noRepositoriesSelected, "no-repos-selected", false, "No repositories can access the organization secret")
 	cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "The value for the secret (reads from standard input if not specified)")
 	cmd.Flags().BoolVar(&opts.DoNotStore, "no-store", false, "Print the encrypted, base64-encoded value instead of storing it on GitHub")
 	cmd.Flags().StringVarP(&opts.EnvFile, "env-file", "f", "", "Load secret names and values from a dotenv-formatted `file`")


Test patch:
diff --git a/acceptance/testdata/secret/secret-org-with-selected-visibility.txtar b/acceptance/testdata/secret/secret-org-with-selected-visibility.txtar
new file mode 100644
index 000000000..9d6bfed84
--- /dev/null
+++ b/acceptance/testdata/secret/secret-org-with-selected-visibility.txtar
@@ -0,0 +1,44 @@
+# Setup environment variables used for testscript
+env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
+env2upper SECRET_NAME=${SCRIPT_NAME}_${RANDOM_STRING}
+
+# Use gh as a credential helper
+exec gh auth setup-git
+
+# Create a repository with a file so it has a default branch
+exec gh repo create ${ORG}/${REPO} --add-readme --private
+
+# Defer repo cleanup
+defer gh repo delete --yes ${ORG}/${REPO}
+
+# Confirm organization secret does not exist, will fail admin:org scope missing
+exec gh secret list --org ${ORG}
+! stdout ${SECRET_NAME}
+
+# Set an organization secret with no shared visibility, but no repos
+exec gh secret set ${SECRET_NAME} --org ${ORG} --body 'just an organization secret' --no-repos-selected
+
+# Defer organization secret cleanup
+defer gh secret delete ${SECRET_NAME} --org ${ORG}
+
+# Verify new organization secret exists with shared visibility
+exec gh api -X GET /orgs/${ORG}/actions/secrets/${SECRET_NAME} --jq '.visibility'
+stdout selected
+
+# Verify the secret is not shared with any repositories
+exec gh api -X GET /orgs/${ORG}/actions/secrets/${SECRET_NAME}/repositories --jq '.repositories | length'
+stdout 0
+
+# Set the same organization secret with shared visibility to the previously created repository
+exec gh secret set ${SECRET_NAME} --org ${ORG} --body 'just an organization secret' --repos ${REPO}
+
+# Verify the secret is now shared with the repository
+exec gh api -X GET /orgs/${ORG}/actions/secrets/${SECRET_NAME}/repositories --jq '.repositories[0].name'
+stdout ${REPO}
+
+# Set the same organization secret with shared visibility back  to no repositories selected
+exec gh secret set ${SECRET_NAME} --org ${ORG} --body 'just an organization secret' --no-repos-selected
+
+# Verify the secret is not shared with any repositories
+exec gh api -X GET /orgs/${ORG}/actions/secrets/${SECRET_NAME}/repositories --jq '.repositories | length'
+stdout 0
diff --git a/acceptance/testdata/secret/secret-org.txtar b/acceptance/testdata/secret/secret-org.txtar
index 7d383009c..3465628b7 100644
--- a/acceptance/testdata/secret/secret-org.txtar
+++ b/acceptance/testdata/secret/secret-org.txtar
@@ -1,4 +1,6 @@
 # Setup environment variables used for testscript
+# This script will most likely fail because you are most likely targeting a repo that is not public and an org
+# that is not on the right plan: https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-an-organization
 env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
 env2upper SECRET_NAME=${SCRIPT_NAME}_${RANDOM_STRING}
 
diff --git a/pkg/cmd/secret/set/set_test.go b/pkg/cmd/secret/set/set_test.go
index 0b305eda6..38c0fb5a9 100644
--- a/pkg/cmd/secret/set/set_test.go
+++ b/pkg/cmd/secret/set/set_test.go
@@ -27,88 +27,120 @@ import (
 
 func TestNewCmdSet(t *testing.T) {
 	tests := []struct {
-		name     string
-		cli      string
-		wants    SetOptions
-		stdinTTY bool
-		wantsErr bool
+		name            string
+		args            string
+		wants           SetOptions
+		stdinTTY        bool
+		wantsErr        bool
+		wantsErrMessage string
 	}{
 		{
 			name:     "invalid visibility",
-			cli:      "cool_secret --org coolOrg -v'mistyVeil'",
+			args:     "cool_secret --org coolOrg -v mistyVeil",
 			wantsErr: true,
 		},
 		{
-			name:     "invalid visibility",
-			cli:      "cool_secret --org coolOrg -v'selected'",
-			wantsErr: true,
+			name:            "when visibility is selected, requires indication of repos",
+			args:            "cool_secret --org coolOrg -v selected",
+			wantsErr:        true,
+			wantsErrMessage: "`--repos` or `--no-repos-selected` required with `--visibility=selected`",
 		},
 		{
-			name:     "repos with wrong vis",
-			cli:      "cool_secret --org coolOrg -v'private' -rcoolRepo",
-			wantsErr: true,
+			name:            "visibilities other than selected do not accept --repos",
+			args:            "cool_secret --org coolOrg -v private -r coolRepo",
+			wantsErr:        true,
+			wantsErrMessage: "`--repos` is only supported with `--visibility=selected`",
+		},
+		{
+			name:            "visibilities other than selected do not accept --no-repos-selected",
+			args:            "cool_secret --org coolOrg -v private --no-repos-selected",
+			wantsErr:        true,
+			wantsErrMessage: "`--no-repos-selected` is only supported with `--visibility=selected`",
+		},
+		{
+			name:            "--repos and --no-repos-selected are mutually exclusive",
+			args:            `--repos coolRepo --no-repos-selected cool_secret`,
+			wantsErr:        true,
+			wantsErrMessage: "specify only one of `--repos` or `--no-repos-selected`",
 		},
 		{
-			name:     "no name",
-			cli:      "",
+			name:     "secret name is required",
+			args:     "",
 			wantsErr: true,
 		},
 		{
-			name:     "multiple names",
-			cli:      "cool_secret good_secret",
+			name:     "multiple positional arguments are not allowed",
+			args:     "cool_secret good_secret",
 			wantsErr: true,
 		},
 		{
-			name:     "visibility without org",
-			cli:      "cool_secret -vall",
+			name:     "visibility is only allowed with --org",
+			args:     "cool_secret -v all",
 			wantsErr: true,
 		},
 		{
-			name: "repos without vis",
-			cli:  "cool_secret -bs --org coolOrg -rcoolRepo",
+			name: "providing --repos without --visibility implies selected visibility",
+			args: "cool_secret --body secret-body --org coolOrg --repos coolRepo",
 			wants: SetOptions{
 				SecretName:      "cool_secret",
 				Visibility:      shared.Selected,
 				RepositoryNames: []string{"coolRepo"},
-				Body:            "s",
+				Body:            "secret-body",
+				OrgName:         "coolOrg",
+			},
+		},
+		{
+			name: "providing --no-repos-selected without --visibility implies selected visibility",
+			args: "cool_secret --body secret-body --org coolOrg --no-repos-selected",
+			wants: SetOptions{
+				SecretName:      "cool_secret",
+				Visibility:      shared.Selected,
+				RepositoryNames: []string{},
+				Body:            "secret-body",
 				OrgName:         "coolOrg",
 			},
 		},
 		{
 			name: "org with selected repo",
-			cli:  "-ocoolOrg -bs -vselected -rcoolRepo cool_secret",
+			args: "-o coolOrg --body secret-body -v selected -r coolRepo cool_secret",
 			wants: SetOptions{
 				SecretName:      "cool_secret",
 				Visibility:      shared.Selected,
 				RepositoryNames: []string{"coolRepo"},
-				Body:            "s",
+				Body:            "secret-body",
 				OrgName:         "coolOrg",
 			},
 		},
 		{
 			name: "org with selected repos",
-			cli:  `--org=coolOrg -bs -vselected -r="coolRepo,radRepo,goodRepo" cool_secret`,
+			args: `--org coolOrg --body secret-body -v selected --repos "coolRepo,radRepo,goodRepo" cool_secret`,
 			wants: SetOptions{
 				SecretName:      "cool_secret",
 				Visibility:      shared.Selected,
 				RepositoryNames: []string{"coolRepo", "goodRepo", "radRepo"},
-				Body:            "s",
+				Body:            "secret-body",
 				OrgName:         "coolOrg",
 			},
 		},
 		{
 			name: "user with selected repos",
-			cli:  `-u -bs -r"monalisa/coolRepo,cli/cli,github/hub" cool_secret`,
+			args: `-u --body secret-body -r "monalisa/coolRepo,cli/cli,github/hub" cool_secret`,
 			wants: SetOptions{
 				SecretName:      "cool_secret",
 				Visibility:      shared.Selected,
 				RepositoryNames: []string{"monalisa/coolRepo", "cli/cli", "github/hub"},
-				Body:            "s",
+				Body:            "secret-body",
 			},
 		},
+		{
+			name:            "--user is mutually exclusive with --no-repos-selected",
+			args:            `-u --no-repos-selected cool_secret`,
+			wantsErr:        true,
+			wantsErrMessage: "`--no-repos-selected` must be omitted when used with `--user`",
+		},
 		{
 			name: "repo",
-			cli:  `cool_secret -b"a secret"`,
+			args: `cool_secret --body "a secret"`,
 			wants: SetOptions{
 				SecretName: "cool_secret",
 				Visibility: shared.Private,
@@ -118,7 +150,7 @@ func TestNewCmdSet(t *testing.T) {
 		},
 		{
 			name: "env",
-			cli:  `cool_secret -b"a secret" -eRelease`,
+			args: `cool_secret --body "a secret" --env Release`,
 			wants: SetOptions{
 				SecretName: "cool_secret",
 				Visibility: shared.Private,
@@ -129,7 +161,7 @@ func TestNewCmdSet(t *testing.T) {
 		},
 		{
 			name: "vis all",
-			cli:  `cool_secret --org coolOrg -b"cool" -vall`,
+			args: `cool_secret --org coolOrg --body "cool" --visibility all`,
 			wants: SetOptions{
 				SecretName: "cool_secret",
 				Visibility: shared.All,
@@ -139,7 +171,7 @@ func TestNewCmdSet(t *testing.T) {
 		},
 		{
 			name: "no store",
-			cli:  `cool_secret --no-store`,
+			args: `cool_secret --no-store`,
 			wants: SetOptions{
 				SecretName: "cool_secret",
 				Visibility: shared.Private,
@@ -148,7 +180,7 @@ func TestNewCmdSet(t *testing.T) {
 		},
 		{
 			name: "Dependabot repo",
-			cli:  `cool_secret -b"a secret" --app Dependabot`,
+			args: `cool_secret --body "a secret" --app Dependabot`,
 			wants: SetOptions{
 				SecretName:  "cool_secret",
 				Visibility:  shared.Private,
@@ -159,19 +191,19 @@ func TestNewCmdSet(t *testing.T) {
 		},
 		{
 			name: "Dependabot org",
-			cli:  "-ocoolOrg -bs -vselected -rcoolRepo cool_secret -aDependabot",
+			args: "--org coolOrg --body secret-body --visibility selected --repos coolRepo cool_secret --app Dependabot",
 			wants: SetOptions{
 				SecretName:      "cool_secret",
 				Visibility:      shared.Selected,
 				RepositoryNames: []string{"coolRepo"},
-				Body:            "s",
+				Body:            "secret-body",
 				OrgName:         "coolOrg",
 				Application:     "Dependabot",
 			},
 		},
 		{
 			name: "Codespaces org",
-			cli:  `random_secret -ocoolOrg -b"random value" -vselected -r"coolRepo,cli/cli" -aCodespaces`,
+			args: `random_secret --org coolOrg --body "random value" --visibility selected --repos "coolRepo,cli/cli" --app Codespaces`,
 			wants: SetOptions{
 				SecretName:      "random_secret",
 				Visibility:      shared.Selected,
@@ -192,7 +224,7 @@ func TestNewCmdSet(t *testing.T) {
 
 			ios.SetStdinTTY(tt.stdinTTY)
 
-			argv, err := shlex.Split(tt.cli)
+			argv, err := shlex.Split(tt.args)
 			assert.NoError(t, err)
 
 			var gotOpts *SetOptions
@@ -208,6 +240,9 @@ func TestNewCmdSet(t *testing.T) {
 			_, err = cmd.ExecuteC()
 			if tt.wantsErr {
 				assert.Error(t, err)
+				if tt.wantsErrMessage != "" {
+					assert.EqualError(t, err, tt.wantsErrMessage)
+				}
 				return
 			}
 			assert.NoError(t, err)
@@ -497,6 +532,16 @@ func Test_setRun_org(t *testing.T) {
 			wantRepositories: []int64{1, 2},
 			wantApp:          "actions",
 		},
+		{
+			name: "no repos visibility",
+			opts: &SetOptions{
+				OrgName:         "UmbrellaCorporation",
+				Visibility:      shared.Selected,
+				RepositoryNames: []string{},
+			},
+			wantRepositories: []int64{},
+			wantApp:          "actions",
+		},
 		{
 			name: "Dependabot",
 			opts: &SetOptions{
@@ -517,6 +562,17 @@ func Test_setRun_org(t *testing.T) {
 			wantDependabotRepositories: []string{"1", "2"},
 			wantApp:                    "dependabot",
 		},
+		{
+			name: "Dependabot no repos visibility",
+			opts: &SetOptions{
+				OrgName:         "UmbrellaCorporation",
+				Visibility:      shared.Selected,
+				Application:     shared.Dependabot,
+				RepositoryNames: []string{},
+			},
+			wantRepositories: []int64{},
+			wantApp:          "dependabot",
+		},
 	}
 
 	for _, tt := range tests {