Repository: cli/cli
Pull request #10846 (2025-04-21 22:26:44)

`gh config`: add config settings for accessible prompter and disabling spinner
### Description

This introduces two new `gh config` options: `accessible_prompter` and `spinner`.

The changes include updates to the configuration handling, mock implementations, and tests to support these new options.

Related:

- https://github.com/cli/cli/pull/10773
- https://github.com/cli/cli/pull/10710


### Acceptance Criteria

> 1. **When** user executes `gh config`
>    **Then** new `accessible_prompter` configuration setting is stated as taking `enabled` or `disabled` as values with default of `disabled`

<details><summary>output</summary>
<p>

```
❯ gh config 
Display or change configuration settings for gh.

Current respected settings:

- `git_protocol`: the protocol to use for git clone and push operations {https|ssh} (default https)
- `editor`: the text editor program to use for authoring text
- `prompt`: toggle interactive prompting in the terminal {enabled|disabled} (default enabled)
- `prefer_editor_prompt`: toggle preference for editor-based interactive prompting in the terminal {enabled|disabled} (default disabled)
- `pager`: the terminal pager program to send standard output to
- `http_unix_socket`: the path to a Unix socket through which to make an HTTP connection
- `browser`: the web browser to use for opening URLs
- `color_labels`: whether to display labels using their RGB hex color codes in terminals that support truecolor {enabled|disabled} (default disabled)
- `accessible_colors`: whether customizable, 4-bit accessible colors should be used {enabled|disabled} (default disabled)
- `accessible_prompter`: whether an accessible prompter should be used {enabled|disabled} (default disabled)
- `spinner`: whether to use a animated spinner as a progress indicator {enabled|disabled} (default enabled)
```

</p>
</details> 

> 1. **Given** user executes `gh config set accessible_prompter enabled`
>    **When** user executes `gh config list`
>    **Then** `accessible_prompter=enabled` is presented

<details><summary>output</summary>
<p>

```
❯ gh config set accessible_prompter enabled
❯ gh config list
git_protocol=https
editor=
prompt=enabled
prefer_editor_prompt=disabled
pager=
http_unix_socket=
browser=
color_labels=disabled
accessible_colors=disabled
accessible_prompter=enabled
spinner=enabled
```

</p>
</details> 

> 1. **Given** user executes `gh config set accessible_prompter enabled`
>    **When** user executes any `gh` command that prompts
>    **Then** user receives an accessible prompter

<details><summary>output</summary>
<p>

```
❯ gh config set accessible_prompter enabled
❯ gh workflow run
Select a workflow
1. Unit and Integration Tests (go.yml)
2. Lint (lint.yml)
3. Code Scanning (codeql.yml)
4. PR Automation (prauto.yml)
5. Issue Automation (issueauto.yml)
6. Deployment (deployment.yml)
7. homebrew-bump-debug (homebrew-bump.yml)
8. Discussion Triage (triage.yml)
9. Integration Tests (integration-tests.yml)
10. Dependabot Updates (dependabot-updates)

Choose: 
```

</p>
</details> 

> 1. **Given** user has no GitHub CLI configuration file
>    **When** user executes `gh` for the first time
>    **Then** the default GitHub CLI configuration file includes `accessible_prompter=disabled`
> 

<details><summary>output</summary>
<p>

```
❯ mv ~/.config/gh/config.yml ~/.config/gh/config.yml.bu
❯ gh config list
git_protocol=https
editor=
prompt=enabled
prefer_editor_prompt=disabled
pager=
http_unix_socket=
browser=
color_labels=disabled
accessible_colors=disabled
accessible_prompter=disabled
spinner=enabled
```

</p>
</details> 


----

> 1. **When** user executes `gh config`
>    **Then** new `spinner` configuration setting is stated as taking `enabled` or `disabled` as values with default of `enabled`

<details><summary>output</summary>
<p>

```
❯ gh config 
Display or change configuration settings for gh.

Current respected settings:

- `git_protocol`: the protocol to use for git clone and push operations {https|ssh} (default https)
- `editor`: the text editor program to use for authoring text
- `prompt`: toggle interactive prompting in the terminal {enabled|disabled} (default enabled)
- `prefer_editor_prompt`: toggle preference for editor-based interactive prompting in the terminal {enabled|disabled} (default disabled)
- `pager`: the terminal pager program to send standard output to
- `http_unix_socket`: the path to a Unix socket through which to make an HTTP connection
- `browser`: the web browser to use for opening URLs
- `color_labels`: whether to display labels using their RGB hex color codes in terminals that support truecolor {enabled|disabled} (default disabled)
- `accessible_colors`: whether customizable, 4-bit accessible colors should be used {enabled|disabled} (default disabled)
- `accessible_prompter`: whether an accessible prompter should be used {enabled|disabled} (default disabled)
- `spinner`: whether to use a animated spinner as a progress indicator {enabled|disabled} (default enabled)
```

</p>
</details> 

> 1. **Given** user executes `gh config set spinner enabled`
>    **When** user executes `gh config list`
>    **Then** `spinner=enabled` is presented

<details><summary>output</summary>
<p>

> [!NOTE]
> Showing both enabling and disabling.

```
❯ gh config set spinner enabled
❯ gh config list
git_protocol=https
editor=
prompt=enabled
prefer_editor_prompt=disabled
pager=
http_unix_socket=
browser=
color_labels=disabled
accessible_colors=disabled
accessible_prompter=disabled
spinner=enabled

❯ gh config set spinner disabled
❯ gh config list
git_protocol=https
editor=
prompt=enabled
prefer_editor_prompt=disabled
pager=
http_unix_socket=
browser=
color_labels=disabled
accessible_colors=disabled
accessible_prompter=disabled
spinner=disabled
```

</p>
</details>  


> 1. **Given** user executes `gh config set spinner disabled`
>    **When** user executes any `gh` command with a progress indicator
>    **Then** user receives a textual progress indicator instead of a spinner

<details><summary>output</summary>
<p>

```
❯ gh config set spinner disabled
❯ gh search prs "test"          
Working...
```

</p>
</details> 

> 1. **Given** user executes `gh config set spinner enabled`
>    **When** user executes any `gh` command with a progress indicator
>    **Then** user receives an animated spinner

<details><summary>output</summary>
<p>

```
❯ gh config set spinner enabled
❯ gh search prs "test"         
⣻
```

</p>
</details> 


> 1. **Given** user has no GitHub CLI configuration file
>    **When** user executes `gh` for the first time
>    **Then** the default GitHub CLI configuration file includes `spinner=enabled`

<details><summary>output</summary>
<p>

```
❯ mv ~/.config/gh/config.yml ~/.config/gh/config.yml.bu

❯ gh config list
git_protocol=https
editor=
prompt=enabled
prefer_editor_prompt=disabled
pager=
http_unix_socket=
browser=
color_labels=disabled
accessible_colors=disabled
accessible_prompter=disabled
spinner=enabled
```

</p>
</details> 




Patch:
diff --git a/internal/config/config.go b/internal/config/config.go
index c78dac15c..003a0ca17 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -18,6 +18,7 @@ import (
 // they are defined here to avoid `cli/cli` being changed unexpectedly.
 const (
 	accessibleColorsKey   = "accessible_colors" // used by cli/go-gh to enable the use of customizable, accessible 4-bit colors.
+	accessiblePrompterKey = "accessible_prompter"
 	aliasesKey            = "aliases"
 	browserKey            = "browser" // used by cli/go-gh to open URLs in web browsers
 	colorLabelsKey        = "color_labels"
@@ -29,6 +30,7 @@ const (
 	pagerKey              = "pager"
 	promptKey             = "prompt"
 	preferEditorPromptKey = "prefer_editor_prompt"
+	spinnerKey            = "spinner"
 	userKey               = "user"
 	usersKey              = "users"
 	versionKey            = "version"
@@ -117,6 +119,11 @@ func (c *cfg) AccessibleColors(hostname string) gh.ConfigEntry {
 	return c.GetOrDefault(hostname, accessibleColorsKey).Unwrap()
 }
 
+func (c *cfg) AccessiblePrompter(hostname string) gh.ConfigEntry {
+	// Intentionally panic if there is no user provided value or default value (which would be a programmer error)
+	return c.GetOrDefault(hostname, accessiblePrompterKey).Unwrap()
+}
+
 func (c *cfg) Browser(hostname string) gh.ConfigEntry {
 	// Intentionally panic if there is no user provided value or default value (which would be a programmer error)
 	return c.GetOrDefault(hostname, browserKey).Unwrap()
@@ -157,6 +164,11 @@ func (c *cfg) PreferEditorPrompt(hostname string) gh.ConfigEntry {
 	return c.GetOrDefault(hostname, preferEditorPromptKey).Unwrap()
 }
 
+func (c *cfg) Spinner(hostname string) gh.ConfigEntry {
+	// Intentionally panic if there is no user provided value or default value (which would be a programmer error)
+	return c.GetOrDefault(hostname, spinnerKey).Unwrap()
+}
+
 func (c *cfg) Version() o.Option[string] {
 	return c.get("", versionKey)
 }
@@ -550,6 +562,10 @@ browser:
 color_labels: disabled
 # Whether customizable, 4-bit accessible colors should be used. Supported values: enabled, disabled
 accessible_colors: disabled
+# Whether an accessible prompter should be used. Supported values: enabled, disabled
+accessible_prompter: disabled
+# Whether to use a animated spinner as a progress indicator. If disabled, a textual progress indicator is used instead. Supported values: enabled, disabled
+spinner: enabled
 `
 
 type ConfigOption struct {
@@ -638,6 +654,24 @@ var Options = []ConfigOption{
 			return c.AccessibleColors(hostname).Value
 		},
 	},
+	{
+		Key:           accessiblePrompterKey,
+		Description:   "whether an accessible prompter should be used",
+		DefaultValue:  "disabled",
+		AllowedValues: []string{"enabled", "disabled"},
+		CurrentValue: func(c gh.Config, hostname string) string {
+			return c.AccessiblePrompter(hostname).Value
+		},
+	},
+	{
+		Key:           spinnerKey,
+		Description:   "whether to use a animated spinner as a progress indicator",
+		DefaultValue:  "enabled",
+		AllowedValues: []string{"enabled", "disabled"},
+		CurrentValue: func(c gh.Config, hostname string) string {
+			return c.Spinner(hostname).Value
+		},
+	},
 }
 
 func HomeDirPath(subdir string) (string, error) {
diff --git a/internal/config/stub.go b/internal/config/stub.go
index 8b9f14290..ea60254db 100644
--- a/internal/config/stub.go
+++ b/internal/config/stub.go
@@ -55,6 +55,9 @@ func NewFromString(cfgStr string) *ghmock.ConfigMock {
 	mock.AccessibleColorsFunc = func(hostname string) gh.ConfigEntry {
 		return cfg.AccessibleColors(hostname)
 	}
+	mock.AccessiblePrompterFunc = func(hostname string) gh.ConfigEntry {
+		return cfg.AccessiblePrompter(hostname)
+	}
 	mock.BrowserFunc = func(hostname string) gh.ConfigEntry {
 		return cfg.Browser(hostname)
 	}
@@ -79,6 +82,9 @@ func NewFromString(cfgStr string) *ghmock.ConfigMock {
 	mock.PreferEditorPromptFunc = func(hostname string) gh.ConfigEntry {
 		return cfg.PreferEditorPrompt(hostname)
 	}
+	mock.SpinnerFunc = func(hostname string) gh.ConfigEntry {
+		return cfg.Spinner(hostname)
+	}
 	mock.VersionFunc = func() o.Option[string] {
 		return cfg.Version()
 	}
diff --git a/internal/gh/gh.go b/internal/gh/gh.go
index 8f3e3cd5b..aa90a5268 100644
--- a/internal/gh/gh.go
+++ b/internal/gh/gh.go
@@ -37,6 +37,8 @@ type Config interface {
 
 	// AccessibleColors returns the configured accessible_colors setting, optionally scoped by host.
 	AccessibleColors(hostname string) ConfigEntry
+	// AccessiblePrompter returns the configured accessible_prompter setting, optionally scoped by host.
+	AccessiblePrompter(hostname string) ConfigEntry
 	// Browser returns the configured browser, optionally scoped by host.
 	Browser(hostname string) ConfigEntry
 	// ColorLabels returns the configured color_label setting, optionally scoped by host.
@@ -53,6 +55,8 @@ type Config interface {
 	Prompt(hostname string) ConfigEntry
 	// PreferEditorPrompt returns the configured editor-based prompt, optionally scoped by host.
 	PreferEditorPrompt(hostname string) ConfigEntry
+	// Spinner returns the configured spinner setting, optionally scoped by host.
+	Spinner(hostname string) ConfigEntry
 
 	// Aliases provides persistent storage and modification of command aliases.
 	Aliases() AliasConfig
diff --git a/internal/gh/mock/config.go b/internal/gh/mock/config.go
index 600eea5c1..9f3f80799 100644
--- a/internal/gh/mock/config.go
+++ b/internal/gh/mock/config.go
@@ -22,6 +22,9 @@ var _ gh.Config = &ConfigMock{}
 //			AccessibleColorsFunc: func(hostname string) gh.ConfigEntry {
 //				panic("mock out the AccessibleColors method")
 //			},
+//			AccessiblePrompterFunc: func(hostname string) gh.ConfigEntry {
+//				panic("mock out the AccessiblePrompter method")
+//			},
 //			AliasesFunc: func() gh.AliasConfig {
 //				panic("mock out the Aliases method")
 //			},
@@ -64,6 +67,9 @@ var _ gh.Config = &ConfigMock{}
 //			SetFunc: func(hostname string, key string, value string)  {
 //				panic("mock out the Set method")
 //			},
+//			SpinnerFunc: func(hostname string) gh.ConfigEntry {
+//				panic("mock out the Spinner method")
+//			},
 //			VersionFunc: func() o.Option[string] {
 //				panic("mock out the Version method")
 //			},
@@ -80,6 +86,9 @@ type ConfigMock struct {
 	// AccessibleColorsFunc mocks the AccessibleColors method.
 	AccessibleColorsFunc func(hostname string) gh.ConfigEntry
 
+	// AccessiblePrompterFunc mocks the AccessiblePrompter method.
+	AccessiblePrompterFunc func(hostname string) gh.ConfigEntry
+
 	// AliasesFunc mocks the Aliases method.
 	AliasesFunc func() gh.AliasConfig
 
@@ -122,6 +131,9 @@ type ConfigMock struct {
 	// SetFunc mocks the Set method.
 	SetFunc func(hostname string, key string, value string)
 
+	// SpinnerFunc mocks the Spinner method.
+	SpinnerFunc func(hostname string) gh.ConfigEntry
+
 	// VersionFunc mocks the Version method.
 	VersionFunc func() o.Option[string]
 
@@ -135,6 +147,11 @@ type ConfigMock struct {
 			// Hostname is the hostname argument value.
 			Hostname string
 		}
+		// AccessiblePrompter holds details about calls to the AccessiblePrompter method.
+		AccessiblePrompter []struct {
+			// Hostname is the hostname argument value.
+			Hostname string
+		}
 		// Aliases holds details about calls to the Aliases method.
 		Aliases []struct {
 		}
@@ -205,6 +222,11 @@ type ConfigMock struct {
 			// Value is the value argument value.
 			Value string
 		}
+		// Spinner holds details about calls to the Spinner method.
+		Spinner []struct {
+			// Hostname is the hostname argument value.
+			Hostname string
+		}
 		// Version holds details about calls to the Version method.
 		Version []struct {
 		}
@@ -213,6 +235,7 @@ type ConfigMock struct {
 		}
 	}
 	lockAccessibleColors   sync.RWMutex
+	lockAccessiblePrompter sync.RWMutex
 	lockAliases            sync.RWMutex
 	lockAuthentication     sync.RWMutex
 	lockBrowser            sync.RWMutex
@@ -227,6 +250,7 @@ type ConfigMock struct {
 	lockPreferEditorPrompt sync.RWMutex
 	lockPrompt             sync.RWMutex
 	lockSet                sync.RWMutex
+	lockSpinner            sync.RWMutex
 	lockVersion            sync.RWMutex
 	lockWrite              sync.RWMutex
 }
@@ -263,6 +287,38 @@ func (mock *ConfigMock) AccessibleColorsCalls() []struct {
 	return calls
 }
 
+// AccessiblePrompter calls AccessiblePrompterFunc.
+func (mock *ConfigMock) AccessiblePrompter(hostname string) gh.ConfigEntry {
+	if mock.AccessiblePrompterFunc == nil {
+		panic("ConfigMock.AccessiblePrompterFunc: method is nil but Config.AccessiblePrompter was just called")
+	}
+	callInfo := struct {
+		Hostname string
+	}{
+		Hostname: hostname,
+	}
+	mock.lockAccessiblePrompter.Lock()
+	mock.calls.AccessiblePrompter = append(mock.calls.AccessiblePrompter, callInfo)
+	mock.lockAccessiblePrompter.Unlock()
+	return mock.AccessiblePrompterFunc(hostname)
+}
+
+// AccessiblePrompterCalls gets all the calls that were made to AccessiblePrompter.
+// Check the length with:
+//
+//	len(mockedConfig.AccessiblePrompterCalls())
+func (mock *ConfigMock) AccessiblePrompterCalls() []struct {
+	Hostname string
+} {
+	var calls []struct {
+		Hostname string
+	}
+	mock.lockAccessiblePrompter.RLock()
+	calls = mock.calls.AccessiblePrompter
+	mock.lockAccessiblePrompter.RUnlock()
+	return calls
+}
+
 // Aliases calls AliasesFunc.
 func (mock *ConfigMock) Aliases() gh.AliasConfig {
 	if mock.AliasesFunc == nil {
@@ -708,6 +764,38 @@ func (mock *ConfigMock) SetCalls() []struct {
 	return calls
 }
 
+// Spinner calls SpinnerFunc.
+func (mock *ConfigMock) Spinner(hostname string) gh.ConfigEntry {
+	if mock.SpinnerFunc == nil {
+		panic("ConfigMock.SpinnerFunc: method is nil but Config.Spinner was just called")
+	}
+	callInfo := struct {
+		Hostname string
+	}{
+		Hostname: hostname,
+	}
+	mock.lockSpinner.Lock()
+	mock.calls.Spinner = append(mock.calls.Spinner, callInfo)
+	mock.lockSpinner.Unlock()
+	return mock.SpinnerFunc(hostname)
+}
+
+// SpinnerCalls gets all the calls that were made to Spinner.
+// Check the length with:
+//
+//	len(mockedConfig.SpinnerCalls())
+func (mock *ConfigMock) SpinnerCalls() []struct {
+	Hostname string
+} {
+	var calls []struct {
+		Hostname string
+	}
+	mock.lockSpinner.RLock()
+	calls = mock.calls.Spinner
+	mock.lockSpinner.RUnlock()
+	return calls
+}
+
 // Version calls VersionFunc.
 func (mock *ConfigMock) Version() o.Option[string] {
 	if mock.VersionFunc == nil {
diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go
index 6ef61cf15..2a4328366 100644
--- a/internal/prompter/prompter.go
+++ b/internal/prompter/prompter.go
@@ -2,13 +2,12 @@ package prompter
 
 import (
 	"fmt"
-	"os"
-	"slices"
 	"strings"
 
 	"github.com/AlecAivazis/survey/v2"
 	"github.com/charmbracelet/huh"
 	"github.com/cli/cli/v2/internal/ghinstance"
+	"github.com/cli/cli/v2/pkg/iostreams"
 	"github.com/cli/cli/v2/pkg/surveyext"
 	ghPrompter "github.com/cli/go-gh/v2/pkg/prompter"
 )
@@ -43,24 +42,21 @@ type Prompter interface {
 	MarkdownEditor(prompt string, defaultValue string, blankAllowed bool) (string, error)
 }
 
-func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWriter, stderr ghPrompter.FileWriter) Prompter {
-	accessiblePrompterValue, accessiblePrompterIsSet := os.LookupEnv("GH_ACCESSIBLE_PROMPTER")
-	falseyValues := []string{"false", "0", "no", ""}
-
-	if accessiblePrompterIsSet && !slices.Contains(falseyValues, accessiblePrompterValue) {
+func New(editorCmd string, io *iostreams.IOStreams) Prompter {
+	if io.AccessiblePrompterEnabled() {
 		return &accessiblePrompter{
-			stdin:     stdin,
-			stdout:    stdout,
-			stderr:    stderr,
+			stdin:     io.In,
+			stdout:    io.Out,
+			stderr:    io.ErrOut,
 			editorCmd: editorCmd,
 		}
 	}
 
 	return &surveyPrompter{
-		prompter:  ghPrompter.New(stdin, stdout, stderr),
-		stdin:     stdin,
-		stdout:    stdout,
-		stderr:    stderr,
+		prompter:  ghPrompter.New(io.In, io.Out, io.ErrOut),
+		stdin:     io.In,
+		stdout:    io.Out,
+		stderr:    io.ErrOut,
 		editorCmd: editorCmd,
 	}
 }
diff --git a/pkg/cmd/factory/default.go b/pkg/cmd/factory/default.go
index 7a45efeb4..52837b252 100644
--- a/pkg/cmd/factory/default.go
+++ b/pkg/cmd/factory/default.go
@@ -227,7 +227,7 @@ func newBrowser(f *cmdutil.Factory) browser.Browser {
 func newPrompter(f *cmdutil.Factory) prompter.Prompter {
 	editor, _ := cmdutil.DetermineEditor(f.Config)
 	io := f.IOStreams
-	return prompter.New(editor, io.In, io.Out, io.ErrOut)
+	return prompter.New(editor, io)
 }
 
 func configFunc() func() (gh.Config, error) {
@@ -284,9 +284,23 @@ func ioStreams(f *cmdutil.Factory) *iostreams.IOStreams {
 		io.SetNeverPrompt(true)
 	}
 
-	ghSpinnerDisabledValue, ghSpinnerDisabledIsSet := os.LookupEnv("GH_SPINNER_DISABLED")
 	falseyValues := []string{"false", "0", "no", ""}
-	if ghSpinnerDisabledIsSet && !slices.Contains(falseyValues, ghSpinnerDisabledValue) {
+
+	accessiblePrompterValue, accessiblePrompterIsSet := os.LookupEnv("GH_ACCESSIBLE_PROMPTER")
+	if accessiblePrompterIsSet {
+		if !slices.Contains(falseyValues, accessiblePrompterValue) {
+			io.SetAccessiblePrompterEnabled(true)
+		}
+	} else if prompt := cfg.AccessiblePrompter(""); prompt.Value == "enabled" {
+		io.SetAccessiblePrompterEnabled(true)
+	}
+
+	ghSpinnerDisabledValue, ghSpinnerDisabledIsSet := os.LookupEnv("GH_SPINNER_DISABLED")
+	if ghSpinnerDisabledIsSet {
+		if !slices.Contains(falseyValues, ghSpinnerDisabledValue) {
+			io.SetSpinnerDisabled(true)
+		}
+	} else if spinnerDisabled := cfg.Spinner(""); spinnerDisabled.Value == "disabled" {
 		io.SetSpinnerDisabled(true)
 	}
 
diff --git a/pkg/cmd/project/shared/queries/queries.go b/pkg/cmd/project/shared/queries/queries.go
index 70da05ab3..46aa58451 100644
--- a/pkg/cmd/project/shared/queries/queries.go
+++ b/pkg/cmd/project/shared/queries/queries.go
@@ -23,7 +23,7 @@ func NewClient(httpClient *http.Client, hostname string, ios *iostreams.IOStream
 	return &Client{
 		apiClient: apiClient,
 		io:        ios,
-		prompter:  prompter.New("", ios.In, ios.Out, ios.ErrOut),
+		prompter:  prompter.New("", ios),
 	}
 }
 
diff --git a/pkg/iostreams/iostreams.go b/pkg/iostreams/iostreams.go
index ba2cc6b50..22f966ac8 100644
--- a/pkg/iostreams/iostreams.go
+++ b/pkg/iostreams/iostreams.go
@@ -58,6 +58,7 @@ type IOStreams struct {
 	progressIndicatorEnabled bool
 	progressIndicator        *spinner.Spinner
 	progressIndicatorMu      sync.Mutex
+	spinnerDisabled          bool
 
 	alternateScreenBufferEnabled bool
 	alternateScreenBufferActive  bool
@@ -78,8 +79,8 @@ type IOStreams struct {
 	pagerCommand string
 	pagerProcess *os.Process
 
-	neverPrompt     bool
-	spinnerDisabled bool
+	neverPrompt               bool
+	accessiblePrompterEnabled bool
 
 	TempFileOverride *os.File
 }
@@ -457,6 +458,14 @@ func (s *IOStreams) AccessibleColorsEnabled() bool {
 	return s.accessibleColorsEnabled
 }
 
+func (s *IOStreams) SetAccessiblePrompterEnabled(enabled bool) {
+	s.accessiblePrompterEnabled = enabled
+}
+
+func (s *IOStreams) AccessiblePrompterEnabled() bool {
+	return s.accessiblePrompterEnabled
+}
+
 func System() *IOStreams {
 	terminal := ghTerm.FromEnv()
 


Test patch:
diff --git a/internal/prompter/accessible_prompter_test.go b/internal/prompter/accessible_prompter_test.go
index 56096972d..619eb14f1 100644
--- a/internal/prompter/accessible_prompter_test.go
+++ b/internal/prompter/accessible_prompter_test.go
@@ -11,6 +11,7 @@ import (
 
 	"github.com/Netflix/go-expect"
 	"github.com/cli/cli/v2/internal/prompter"
+	"github.com/cli/cli/v2/pkg/iostreams"
 	"github.com/creack/pty"
 	"github.com/hinshun/vt10x"
 	"github.com/stretchr/testify/assert"
@@ -33,7 +34,7 @@ import (
 func TestAccessiblePrompter(t *testing.T) {
 	t.Run("Select", func(t *testing.T) {
 		console := newTestVirtualTerminal(t)
-		p := newTestAcessiblePrompter(t, console)
+		p := newTestAccessiblePrompter(t, console)
 
 		go func() {
 			// Wait for prompt to appear
@@ -52,7 +53,7 @@ func TestAccessiblePrompter(t *testing.T) {
 
 	t.Run("MultiSelect", func(t *testing.T) {
 		console := newTestVirtualTerminal(t)
-		p := newTestAcessiblePrompter(t, console)
+		p := newTestAccessiblePrompter(t, console)
 
 		go func() {
 			// Wait for prompt to appear
@@ -77,7 +78,7 @@ func TestAccessiblePrompter(t *testing.T) {
 
 	t.Run("Input", func(t *testing.T) {
 		console := newTestVirtualTerminal(t)
-		p := newTestAcessiblePrompter(t, console)
+		p := newTestAccessiblePrompter(t, console)
 		dummyText := "12345abcdefg"
 
 		go func() {
@@ -97,7 +98,7 @@ func TestAccessiblePrompter(t *testing.T) {
 
 	t.Run("Input - blank input returns default value", func(t *testing.T) {
 		console := newTestVirtualTerminal(t)
-		p := newTestAcessiblePrompter(t, console)
+		p := newTestAccessiblePrompter(t, console)
 		dummyDefaultValue := "12345abcdefg"
 
 		go func() {
@@ -117,7 +118,7 @@ func TestAccessiblePrompter(t *testing.T) {
 
 	t.Run("Password", func(t *testing.T) {
 		console := newTestVirtualTerminal(t)
-		p := newTestAcessiblePrompter(t, console)
+		p := newTestAccessiblePrompter(t, console)
 		dummyPassword := "12345abcdefg"
 
 		go func() {
@@ -137,7 +138,7 @@ func TestAccessiblePrompter(t *testing.T) {
 
 	t.Run("Confirm", func(t *testing.T) {
 		console := newTestVirtualTerminal(t)
-		p := newTestAcessiblePrompter(t, console)
+		p := newTestAccessiblePrompter(t, console)
 
 		go func() {
 			// Wait for prompt to appear
@@ -156,7 +157,7 @@ func TestAccessiblePrompter(t *testing.T) {
 
 	t.Run("Confirm - blank input returns default", func(t *testing.T) {
 		console := newTestVirtualTerminal(t)
-		p := newTestAcessiblePrompter(t, console)
+		p := newTestAccessiblePrompter(t, console)
 
 		go func() {
 			// Wait for prompt to appear
@@ -175,7 +176,7 @@ func TestAccessiblePrompter(t *testing.T) {
 
 	t.Run("AuthToken", func(t *testing.T) {
 		console := newTestVirtualTerminal(t)
-		p := newTestAcessiblePrompter(t, console)
+		p := newTestAccessiblePrompter(t, console)
 		dummyAuthToken := "12345abcdefg"
 
 		go func() {
@@ -195,7 +196,7 @@ func TestAccessiblePrompter(t *testing.T) {
 
 	t.Run("AuthToken - blank input returns error", func(t *testing.T) {
 		console := newTestVirtualTerminal(t)
-		p := newTestAcessiblePrompter(t, console)
+		p := newTestAccessiblePrompter(t, console)
 		dummyAuthTokenForAfterFailure := "12345abcdefg"
 
 		go func() {
@@ -223,7 +224,7 @@ func TestAccessiblePrompter(t *testing.T) {
 
 	t.Run("ConfirmDeletion", func(t *testing.T) {
 		console := newTestVirtualTerminal(t)
-		p := newTestAcessiblePrompter(t, console)
+		p := newTestAccessiblePrompter(t, console)
 
 		requiredValue := "test"
 		go func() {
@@ -243,7 +244,7 @@ func TestAccessiblePrompter(t *testing.T) {
 
 	t.Run("ConfirmDeletion - bad input", func(t *testing.T) {
 		console := newTestVirtualTerminal(t)
-		p := newTestAcessiblePrompter(t, console)
+		p := newTestAccessiblePrompter(t, console)
 		requiredValue := "test"
 		badInputValue := "garbage"
 
@@ -272,7 +273,7 @@ func TestAccessiblePrompter(t *testing.T) {
 
 	t.Run("InputHostname", func(t *testing.T) {
 		console := newTestVirtualTerminal(t)
-		p := newTestAcessiblePrompter(t, console)
+		p := newTestAccessiblePrompter(t, console)
 		hostname := "example.com"
 
 		go func() {
@@ -292,7 +293,7 @@ func TestAccessiblePrompter(t *testing.T) {
 
 	t.Run("MarkdownEditor - blank allowed with blank input returns blank", func(t *testing.T) {
 		console := newTestVirtualTerminal(t)
-		p := newTestAcessiblePrompter(t, console)
+		p := newTestAccessiblePrompter(t, console)
 
 		go func() {
 			// Wait for prompt to appear
@@ -311,7 +312,7 @@ func TestAccessiblePrompter(t *testing.T) {
 
 	t.Run("MarkdownEditor - blank disallowed with default value returns default value", func(t *testing.T) {
 		console := newTestVirtualTerminal(t)
-		p := newTestAcessiblePrompter(t, console)
+		p := newTestAccessiblePrompter(t, console)
 		defaultValue := "12345abcdefg"
 
 		go func() {
@@ -339,7 +340,7 @@ func TestAccessiblePrompter(t *testing.T) {
 
 	t.Run("MarkdownEditor - blank disallowed no default value returns error", func(t *testing.T) {
 		console := newTestVirtualTerminal(t)
-		p := newTestAcessiblePrompter(t, console)
+		p := newTestAccessiblePrompter(t, console)
 
 		go func() {
 			// Wait for prompt to appear
@@ -419,21 +420,40 @@ func newTestVirtualTerminal(t *testing.T) *expect.Console {
 	return console
 }
 
-func newTestAcessiblePrompter(t *testing.T, console *expect.Console) prompter.Prompter {
+func newTestVirtualTerminalIOStreams(t *testing.T, console *expect.Console) *iostreams.IOStreams {
 	t.Helper()
+	io := &iostreams.IOStreams{
+		In:     console.Tty(),
+		Out:    console.Tty(),
+		ErrOut: console.Tty(),
+	}
+	io.SetStdinTTY(false)
+	io.SetStdoutTTY(false)
+	io.SetStderrTTY(false)
+	return io
+}
 
-	t.Setenv("GH_ACCESSIBLE_PROMPTER", "true")
-	// `echo`` is chose as the editor command because it immediately returns
-	// a success exit code, returns an empty string, doesn't require any user input,
-	// and since this file is only built on Linux, it is near guaranteed to be available.
-	return prompter.New("echo", console.Tty(), console.Tty(), console.Tty())
+// `echo` is chosen as the editor command because it immediately returns
+// a success exit code, returns an empty string, doesn't require any user input,
+// and since this file is only built on Linux, it is near guaranteed to be available.
+var editorCmd = "echo"
+
+func newTestAccessiblePrompter(t *testing.T, console *expect.Console) prompter.Prompter {
+	t.Helper()
+
+	io := newTestVirtualTerminalIOStreams(t, console)
+	io.SetAccessiblePrompterEnabled(true)
+
+	return prompter.New(editorCmd, io)
 }
 
 func newTestSurveyPrompter(t *testing.T, console *expect.Console) prompter.Prompter {
 	t.Helper()
 
-	t.Setenv("GH_ACCESSIBLE_PROMPTER", "false")
-	return prompter.New("echo", console.Tty(), console.Tty(), console.Tty())
+	io := newTestVirtualTerminalIOStreams(t, console)
+	io.SetAccessiblePrompterEnabled(false)
+
+	return prompter.New(editorCmd, io)
 }
 
 // failOnExpectError adds an observer that will fail the test in a standardised way
diff --git a/pkg/cmd/config/list/list_test.go b/pkg/cmd/config/list/list_test.go
index 2a1dd72ee..27260e857 100644
--- a/pkg/cmd/config/list/list_test.go
+++ b/pkg/cmd/config/list/list_test.go
@@ -102,6 +102,8 @@ func Test_listRun(t *testing.T) {
 				browser=brave
 				color_labels=disabled
 				accessible_colors=disabled
+				accessible_prompter=disabled
+				spinner=enabled
 			`),
 		},
 	}
diff --git a/pkg/cmd/factory/default_test.go b/pkg/cmd/factory/default_test.go
index 5036a1dc1..d7bfe39fd 100644
--- a/pkg/cmd/factory/default_test.go
+++ b/pkg/cmd/factory/default_test.go
@@ -435,6 +435,7 @@ func Test_ioStreams_prompt(t *testing.T) {
 func Test_ioStreams_spinnerDisabled(t *testing.T) {
 	tests := []struct {
 		name            string
+		config          gh.Config
 		spinnerDisabled bool
 		env             map[string]string
 	}{
@@ -442,6 +443,16 @@ func Test_ioStreams_spinnerDisabled(t *testing.T) {
 			name:            "default config",
 			spinnerDisabled: false,
 		},
+		{
+			name:            "config with spinner disabled",
+			config:          disableSpinnersConfig(),
+			spinnerDisabled: true,
+		},
+		{
+			name:            "config with spinner enabled",
+			config:          enableSpinnersConfig(),
+			spinnerDisabled: false,
+		},
 		{
 			name:            "spinner disabled via GH_SPINNER_DISABLED env var = 0",
 			env:             map[string]string{"GH_SPINNER_DISABLED": "0"},
@@ -467,6 +478,18 @@ func Test_ioStreams_spinnerDisabled(t *testing.T) {
 			env:             map[string]string{"GH_SPINNER_DISABLED": "true"},
 			spinnerDisabled: true,
 		},
+		{
+			name:            "config enabled but env disabled, respects env",
+			config:          enableSpinnersConfig(),
+			env:             map[string]string{"GH_SPINNER_DISABLED": "true"},
+			spinnerDisabled: true,
+		},
+		{
+			name:            "config disabled but env enabled, respects env",
+			config:          disableSpinnersConfig(),
+			env:             map[string]string{"GH_SPINNER_DISABLED": "false"},
+			spinnerDisabled: false,
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -474,12 +497,87 @@ func Test_ioStreams_spinnerDisabled(t *testing.T) {
 				t.Setenv(k, v)
 			}
 			f := New("1")
+			f.Config = func() (gh.Config, error) {
+				if tt.config == nil {
+					return config.NewBlankConfig(), nil
+				} else {
+					return tt.config, nil
+				}
+			}
 			io := ioStreams(f)
 			assert.Equal(t, tt.spinnerDisabled, io.GetSpinnerDisabled())
 		})
 	}
 }
 
+func Test_ioStreams_accessiblePrompterEnabled(t *testing.T) {
+	tests := []struct {
+		name                      string
+		config                    gh.Config
+		accessiblePrompterEnabled bool
+		env                       map[string]string
+	}{
+		{
+			name:                      "default config",
+			accessiblePrompterEnabled: false,
+		},
+		{
+			name:                      "config with accessible prompter enabled",
+			config:                    enableAccessiblePrompterConfig(),
+			accessiblePrompterEnabled: true,
+		},
+		{
+			name:                      "config with accessible prompter disabled",
+			config:                    disableAccessiblePrompterConfig(),
+			accessiblePrompterEnabled: false,
+		},
+		{
+			name:                      "accessible prompter enabled via GH_ACCESSIBLE_PROMPTER env var = 1",
+			env:                       map[string]string{"GH_ACCESSIBLE_PROMPTER": "1"},
+			accessiblePrompterEnabled: true,
+		},
+		{
+			name:                      "accessible prompter enabled via GH_ACCESSIBLE_PROMPTER env var = true",
+			env:                       map[string]string{"GH_ACCESSIBLE_PROMPTER": "true"},
+			accessiblePrompterEnabled: true,
+		},
+		{
+			name:                      "accessible prompter disabled via GH_ACCESSIBLE_PROMPTER env var = 0",
+			env:                       map[string]string{"GH_ACCESSIBLE_PROMPTER": "0"},
+			accessiblePrompterEnabled: false,
+		},
+		{
+			name:                      "config disabled but env enabled, respects env",
+			config:                    disableAccessiblePrompterConfig(),
+			env:                       map[string]string{"GH_ACCESSIBLE_PROMPTER": "true"},
+			accessiblePrompterEnabled: true,
+		},
+		{
+			name:                      "config enabled but env disabled, respects env",
+			config:                    enableAccessiblePrompterConfig(),
+			env:                       map[string]string{"GH_ACCESSIBLE_PROMPTER": "false"},
+			accessiblePrompterEnabled: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			for k, v := range tt.env {
+				t.Setenv(k, v)
+			}
+			f := New("1")
+			f.Config = func() (gh.Config, error) {
+				if tt.config == nil {
+					return config.NewBlankConfig(), nil
+				} else {
+					return tt.config, nil
+				}
+			}
+			io := ioStreams(f)
+			assert.Equal(t, tt.accessiblePrompterEnabled, io.AccessiblePrompterEnabled())
+		})
+	}
+}
+
 func Test_ioStreams_colorLabels(t *testing.T) {
 	tests := []struct {
 		name               string
@@ -664,6 +762,22 @@ func disablePromptConfig() gh.Config {
 	return config.NewFromString("prompt: disabled")
 }
 
+func enableAccessiblePrompterConfig() gh.Config {
+	return config.NewFromString("accessible_prompter: enabled")
+}
+
+func disableAccessiblePrompterConfig() gh.Config {
+	return config.NewFromString("accessible_prompter: disabled")
+}
+
+func disableSpinnersConfig() gh.Config {
+	return config.NewFromString("spinner: disabled")
+}
+
+func enableSpinnersConfig() gh.Config {
+	return config.NewFromString("spinner: enabled")
+}
+
 func disableColorLabelsConfig() gh.Config {
 	return config.NewFromString("color_labels: disabled")
 }