# fix(redact): order OPF prompt defaults

`1c559e1`→[main](/content/gh/entireio/cli/commits/main/index.html)·

peyton-alt·4w ago·7 files·+59 added/-12 removed

## Sessions

bbf03b229665View transcript

[?\
OpenAI Privacy Filter Prompt DefaultsCodex·GPT-5.5·5 steps](/content/gh/entireio/cli/session/019edbd8-73f8-7981-8071-4156cbfaa27e#timeline-bbf03b229665/index.html)

## Changes

7

- cmd/entire/cli

- settings

- Msettings.go+5/-5

- Msettings_test.go+2/-2

- strategy

- Mglobal_test.go+3

- Mmanual_commit_opf_prompt.go+2/-2

- Mmanual_commit_opf_prompt_test.go+42/-1

- Mmanual_commit_push.go+4/-1

- docs

- Msecurity-and-privacy.md+1/-1

```
242 unmodified lines

243
244
245
246
247
246
247
248
249
250
1 unmodified line

252
253
254
255
255
256
257
258
259
733 unmodified lines

993
994
995
996
996
997
998
999
1000
1000
1001
1002
1003

242 unmodified lines

// PromptDefault controls whether the pre-push hook asks the user
	// before running OPF. "" (default) and "ask" both surface the
	// interactive prompt; "always" runs without asking; "never" skips
	// OPF and pushes 7-layer content. ENTIRE_OPF=yes|no on the push
	// interactive prompt; "never" skips OPF and pushes 7-layer content;
	// "always" runs without asking. ENTIRE_OPF=yes|no on the push
	// invocation overrides this setting per-push.
	PromptDefault string `json:"prompt_default,omitempty"`
}
1 unmodified line

// Valid PromptDefault values. Empty == OPFPromptAsk.
const (
	OPFPromptAsk    = "ask"
	OPFPromptAlways = "always"
	OPFPromptNever  = "never"
	OPFPromptAlways = "always"
)

// GetCommitLinking returns the effective commit linking mode.
733 unmodified lines

return fmt.Errorf("openai_privacy_filter.timeout_seconds must be greater than or equal to 0 (got %d)", opf.TimeoutSeconds)
}
switch opf.PromptDefault {
case "", OPFPromptAsk, OPFPromptAlways, OPFPromptNever:
case "", OPFPromptAsk, OPFPromptNever, OPFPromptAlways:
	// ok
default:
	return fmt.Errorf("openai_privacy_filter.prompt_default must be one of %q, %q, %q (got %q)",
		OPFPromptAsk, OPFPromptAlways, OPFPromptNever, opf.PromptDefault)
		OPFPromptAsk, OPFPromptNever, OPFPromptAlways, opf.PromptDefault)
}
return nil
}
```

Mcmd/entire/cli/settings/settings.go+5/-5

```
954 unmodified lines

955
956
957
958
958
959
960
961
3 unmodified lines

965
966
967
968
968
969
970
971
972

954 unmodified lines

// TestLoadFromBytes_OPFSettings_PromptDefault covers parsing + validation
// of the prompt_default field added for the pre-push prompt UX. Empty is
// allowed (treated as "ask"); ask/always/never are the only valid values.
// allowed (treated as "ask"); ask/never/always are the only valid values.
func TestLoadFromBytes_OPFSettings_PromptDefault(t *testing.T) {
	t.Parallel()
	cases := []struct {
		wantVal string
	}{
		{name: "ask", value: `"ask"`, wantVal: "ask"},
		{name: "always", value: `"always"`, wantVal: "always"},
		{name: "never", value: `"never"`, wantVal: "never"},
		{name: "always", value: `"always"`, wantVal: "always"},
		{name: "empty_string_allowed_as_ask", value: `""`, wantVal: ""},
		{name: "bogus_value_rejected", value: `"sometimes"`, wantErr: true},
	}
```

Mcmd/entire/cli/settings/settings_test.go+2/-2

```
1 unmodified line

2
3
4
5
6
7
8
4 unmodified lines

13
14
15
16
17
18
19
20

1 unmodified line

import (
	"fmt"
	"io"
	"os"
	"testing"

4 unmodified lines

)

func TestMain(m *testing.M) {
	opfPrePushProgressWriter = io.Discard

// Register a default ConfigSource so tests that call ConfigScoped
	// (directly or indirectly via Commit/CreateTag) don't fail with
	// "no config loader registered".
```

Mcmd/entire/cli/strategy/global_test.go+3

```
40 unmodified lines

41
42
43
44
45
44
45
46
47
48
49
50

40 unmodified lines

return OPFSkip, nil
	}
switch strings.ToLower(strings.TrimSpace(promptDefault)) {
case settings.OPFPromptAlways:
	return OPFRun, nil
case settings.OPFPromptNever:
	return OPFSkip, nil
case settings.OPFPromptAlways:
	return OPFRun, nil
}
if !hasTTY {
	// Non-interactive context: run OPF (matches the user's "if
```

Mcmd/entire/cli/strategy/manual_commit_opf_prompt.go+2/-2

```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
30 unmodified lines

49
50
51
49
52
53
54
55
56
94 unmodified lines

151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191

package strategy

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"io"
	"os"
	"path/filepath"
	"testing"

"github.com/entireio/cli/cmd/entire/cli/paths"
	"github.com/entireio/cli/cmd/entire/cli/settings"
	"github.com/entireio/cli/cmd/entire/cli/testutil"
	"github.com/stretchr/testify/require"
)

30 unmodified lines

{name: "env_yes_case_insensitive", env: "YES", promptDefault: "", hasTTY: false, prompter: promptNever, want: OPFRun},
	{name: "env_no_with_whitespace", env: "  no  ", promptDefault: "", hasTTY: false, prompter: promptNever, want: OPFSkip},
	// Setting wins over prompt
	{name: "setting_always_skips_prompt", env: "", promptDefault: settings.OPFPromptAlways, hasTTY: true, prompter: promptNever, want: OPFRun},
	{name: "setting_never_skips_prompt", env: "", promptDefault: settings.OPFPromptNever, hasTTY: true, prompter: promptNever, want: OPFSkip},
	{name: "setting_always_skips_prompt", env: "", promptDefault: settings.OPFPromptAlways, hasTTY: true, prompter: promptNever, want: OPFRun},
	// Non-TTY fallback: run (matches the "if enabled, just run" semantics)
	{name: "no_tty_auto_runs", env: "", promptDefault: "", hasTTY: false, prompter: promptNever, want: OPFRun},
	{name: "no_tty_ignores_ask_setting", env: "", promptDefault: settings.OPFPromptAsk, hasTTY: false, prompter: promptNever, want: OPFRun},
94 unmodified lines

require.NoError(t, json.Unmarshal(got, &parsed))
	require.Equal(t, settings.OPFPromptAlways, parsed.Redaction.OPF.PromptDefault)
}

// TestPrePush_OPFProgressUsesConfiguredWriter pins the test-noise escape hatch:
// PrePush still emits the non-interactive OPF progress notice in production,
// but tests can redirect it away from process stderr.
func TestPrePush_OPFProgressUsesConfiguredWriter(t *testing.T) {
	tmpDir := t.TempDir()
	testutil.InitRepo(t, tmpDir)
	testutil.WriteFile(t, tmpDir, "f.txt", "init")
	testutil.GitAdd(t, tmpDir, "f.txt")
	testutil.GitCommit(t, tmpDir, "init")
	require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, paths.EntireDir), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(tmpDir, paths.EntireDir, "settings.json"), []byte(`{
  "enabled": true,
  "redaction": {
    "openai_privacy_filter": {
      "enabled": true,
      "categories": {"private_person": true}
    }
  }
}`), 0o644))
	 t.Chdir(tmpDir)
	configureFakeOPF(t, &fakeOPFForRewrite{})
	
	var out bytes.Buffer
	withOPFPrePushProgressWriterForTest(t, &out)

require.NoError(t, (&ManualCommitStrategy{}).PrePush(t.Context(), "origin"))
	require.Contains(t, out.String(), "OpenAI Privacy Filter: scanning checkpoints before push")
}

func withOPFPrePushProgressWriterForTest(t testing.TB, w io.Writer) {
	t.Helper()
	previous := opfPrePushProgressWriter
	opfPrePushProgressWriter = w
	t.Cleanup(func() {
		opfPrePushProgressWriter = previous
	})
}
```

Mcmd/entire/cli/strategy/manual_commit_opf_prompt_test.go+42/-1

```
2 unmodified lines

3
4
5
6
7
8
9
9 unmodified lines

19
20
21
22
23
24
25
26
29 unmodified lines

56
57
58
56
59
60
61
62

2 unmodified lines

import (
	"context"
	"errors"
	"io"
	"log/slog"
	"os"

9 unmodified lines

// command propagates the non-zero exit code so git push aborts.
var errOPFAbortedByUser = errors.New("OPF prompt aborted by user; push cancelled")

var opfPrePushProgressWriter io.Writer = os.Stderr

// PrePush is called by the git pre-push hook before pushing to a remote.
// It pushes each ref in refs.Push alongside the user's push.
//
29 unmodified lines

if cfg != nil && cfg.Redaction != nil {
		opfCfg = cfg.Redaction.OpenAIPrivacyFilter
	}
decision, decisionErr := resolveOPFDecisionForPrePush(ctx, opfCfg, os.Stderr)
decision, decisionErr := resolveOPFDecisionForPrePush(ctx, opfCfg, opfPrePushProgressWriter)
	if decisionErr != nil {
		logging.Warn(ctx, "OPF pre-push decision failed; aborting push",
			slog.String("error", decisionErr.Error()),
```

Mcmd/entire/cli/strategy/manual_commit_push.go+4/-1

```
127 unmodified lines

128
129
130
131
131
132
133
134

127 unmodified lines

- `command` — path or PATH-resolvable name of the `opf` binary. Defaults to `opf`.
- `timeout_seconds` — per-invocation timeout. Defaults to `30`.
- `prompt_default` — `"ask"` (default), `"always"`, or `"never"`. Controls whether the pre-push hook surfaces an interactive prompt before running OPF. `ENTIRE_OPF=yes` or `ENTIRE_OPF=no` on a single `git push` invocation overrides this for that push only.
- `prompt_default` — `"ask"` (default), `"never"`, or `"always"`. Controls whether the pre-push hook surfaces an interactive prompt before running OPF. `ENTIRE_OPF=yes` or `ENTIRE_OPF=no` on a single `git push` invocation overrides this for that push only.

The interactive prompt offers three options and reacts to **Ctrl-C** for cancellation:
```

Mdocs/security-and-privacy.md+1/-1
