Skip to content

Commit 7e7240a

Browse files
Merge pull request #28 from miguelmartens/feat/brave-beta
Enhance Brave Browser support and documentation
2 parents fd6fdb6 + 0488a7a commit 7e7240a

9 files changed

Lines changed: 168 additions & 44 deletions

File tree

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Inspired by [SlimBrave](https://github.com/ltx0101/SlimBrave), [Debloat Brave Br
1212

1313
- **macOS** only today (uses `defaults` and `~/Library/Preferences/com.brave.Browser.plist`)
1414
- **Go 1.25.6+** to build
15-
- **Brave Browser** installed in `/Applications/Brave Browser.app` (tested with Brave stable; policy keys may vary by Brave version)
15+
- **Brave Browser** installed in `/Applications/Brave Browser.app` (or **Brave Browser Beta** in `/Applications/Brave Browser Beta.app` with `--beta`; policy keys may vary by Brave version)
1616

1717
**Platform support:** Cowardly currently supports **macOS only**. Support for **Linux** and **Windows** may be added in the future; on those platforms Brave uses different policy mechanisms (e.g. JSON on Linux, registry/Group Policy on Windows). See **[docs/PLATFORMS.md](docs/PLATFORMS.md)** for details and contribution notes.
1818

@@ -147,6 +147,14 @@ After applying or resetting, **restart Brave Browser** for changes to take effec
147147
cowardly -r
148148
```
149149

150+
- **Target Brave Browser Beta** — Use `--beta` with any command to manage Brave Beta instead of stable:
151+
152+
```bash
153+
cowardly --beta --apply
154+
cowardly --beta --reset
155+
cowardly --beta
156+
```
157+
150158
- **Print current settings** (user and enforced/managed when present)
151159

152160
```bash

cmd/cowardly/main.go

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ func main() {
2525
}
2626

2727
args := os.Args[1:]
28+
for _, arg := range args {
29+
if strings.TrimLeft(arg, "-") == "beta" {
30+
brave.UseBeta(true)
31+
break
32+
}
33+
}
2834
for _, arg := range args {
2935
arg = strings.TrimLeft(arg, "-")
3036
switch {
@@ -91,7 +97,11 @@ func main() {
9197
}
9298

9399
if !brave.BraveInstalled() {
94-
fmt.Fprintln(os.Stderr, "Brave Browser not found in /Applications. Install Brave first.")
100+
which := "Brave Browser"
101+
if brave.IsBeta() {
102+
which = "Brave Browser Beta"
103+
}
104+
fmt.Fprintf(os.Stderr, "%s not found in /Applications. Install Brave first.\n", which)
95105
os.Exit(1)
96106
}
97107

@@ -104,10 +114,14 @@ func main() {
104114

105115
func versionInfo() {
106116
fmt.Printf("Cowardly version: %s\n", Version)
117+
which := "Brave"
118+
if brave.IsBeta() {
119+
which = "Brave Beta"
120+
}
107121
if v := brave.BraveVersion(); v != "" {
108-
fmt.Printf("Brave version: %s\n", v)
122+
fmt.Printf("%s version: %s\n", which, v)
109123
} else {
110-
fmt.Println("Brave version: (not installed or unknown)")
124+
fmt.Printf("%s version: (not installed or unknown)\n", which)
111125
}
112126
}
113127

@@ -206,7 +220,11 @@ func applyPrivacyGuides(basePresetID string) {
206220
basePresetID = presets.PrivacyGuidesBasePresetID
207221
}
208222
if !brave.BraveInstalled() {
209-
fmt.Fprintln(os.Stderr, "Brave Browser not found in /Applications.")
223+
which := "Brave Browser"
224+
if brave.IsBeta() {
225+
which = "Brave Browser Beta"
226+
}
227+
fmt.Fprintf(os.Stderr, "%s not found in /Applications.\n", which)
210228
os.Exit(1)
211229
}
212230
if brave.BraveRunning() {
@@ -238,7 +256,11 @@ func applyPrivacyGuides(basePresetID string) {
238256

239257
func applyPreset(presetID string) {
240258
if !brave.BraveInstalled() {
241-
fmt.Fprintln(os.Stderr, "Brave Browser not found in /Applications.")
259+
which := "Brave Browser"
260+
if brave.IsBeta() {
261+
which = "Brave Browser Beta"
262+
}
263+
fmt.Fprintf(os.Stderr, "%s not found in /Applications.\n", which)
242264
os.Exit(1)
243265
}
244266
if brave.BraveRunning() {
@@ -269,7 +291,11 @@ func applyPreset(presetID string) {
269291

270292
func applyFile(path string) {
271293
if !brave.BraveInstalled() {
272-
fmt.Fprintln(os.Stderr, "Brave Browser not found in /Applications.")
294+
which := "Brave Browser"
295+
if brave.IsBeta() {
296+
which = "Brave Browser Beta"
297+
}
298+
fmt.Fprintf(os.Stderr, "%s not found in /Applications.\n", which)
273299
os.Exit(1)
274300
}
275301
if brave.BraveRunning() {
@@ -437,7 +463,11 @@ func deleteBackup(path string) {
437463

438464
func reapply() {
439465
if !brave.BraveInstalled() {
440-
fmt.Fprintln(os.Stderr, "Brave Browser not found in /Applications.")
466+
which := "Brave Browser"
467+
if brave.IsBeta() {
468+
which = "Brave Browser Beta"
469+
}
470+
fmt.Fprintf(os.Stderr, "%s not found in /Applications.\n", which)
441471
os.Exit(1)
442472
}
443473
if brave.BraveRunning() {
@@ -494,7 +524,16 @@ func installLoginHook() {
494524
if err != nil {
495525
cowardlyPath = "cowardly" // fallback to PATH
496526
}
527+
reapplyArgs := []string{"--reapply"}
528+
if brave.IsBeta() {
529+
reapplyArgs = []string{"--beta", "--reapply"}
530+
}
497531
plistPath := filepath.Join(launchAgentDir, "com.cowardly.reapply.plist")
532+
programArgs := append([]string{cowardlyPath}, reapplyArgs...)
533+
programArgsXML := ""
534+
for _, a := range programArgs {
535+
programArgsXML += fmt.Sprintf(" <string>%s</string>\n", escapePlistString(a))
536+
}
498537
plist := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
499538
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
500539
<plist version="1.0">
@@ -503,9 +542,7 @@ func installLoginHook() {
503542
<string>com.cowardly.reapply</string>
504543
<key>ProgramArguments</key>
505544
<array>
506-
<string>%s</string>
507-
<string>--reapply</string>
508-
</array>
545+
%s </array>
509546
<key>RunAtLoad</key>
510547
<true/>
511548
<key>StandardErrorPath</key>
@@ -514,7 +551,7 @@ func installLoginHook() {
514551
<string>%s/reapply.log</string>
515552
</dict>
516553
</plist>
517-
`, cowardlyPath, dir, dir)
554+
`, programArgsXML, dir, dir)
518555
if err := os.WriteFile(plistPath, []byte(plist), 0644); err != nil {
519556
fmt.Fprintf(os.Stderr, "install-login-hook: %v\n", err)
520557
os.Exit(1)
@@ -524,6 +561,16 @@ func installLoginHook() {
524561
fmt.Println("To remove: rm", plistPath)
525562
}
526563

564+
// escapePlistString escapes a string for use inside a plist <string> element.
565+
func escapePlistString(s string) string {
566+
s = strings.ReplaceAll(s, "&", "&amp;")
567+
s = strings.ReplaceAll(s, "<", "&lt;")
568+
s = strings.ReplaceAll(s, ">", "&gt;")
569+
s = strings.ReplaceAll(s, "\"", "&quot;")
570+
s = strings.ReplaceAll(s, "'", "&apos;")
571+
return s
572+
}
573+
527574
// resolveBackupPath returns the full path if path is a filename matching a backup, or path if it's already a full path that exists.
528575
func resolveBackupPath(path string) string {
529576
path = strings.TrimSpace(path)
@@ -551,6 +598,7 @@ func printUsage() {
551598
552599
Usage:
553600
cowardly Start the TUI
601+
cowardly --beta Target Brave Browser Beta (use with any command)
554602
cowardly --apply, -a Apply Quick Debloat preset and exit
555603
cowardly --apply=<id> Apply preset by ID (e.g. quick, max-privacy)
556604
cowardly --privacy-guides [=base] Apply Privacy Guides supplement (default base: quick)
@@ -568,5 +616,5 @@ Usage:
568616
cowardly --delete-backup=<path> Delete a backup file
569617
cowardly --help, -h Show this help
570618
571-
Restart Brave Browser after applying or resetting settings.`)
619+
Use --beta to target Brave Browser Beta instead of stable. Restart Brave after applying or resetting settings.`)
572620
}

docs/FEATURES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ This document summarizes what Cowardly currently does. For possible future work,
55
## Core: policy application
66

77
- **Apply settings** — Write Brave policy keys (bool, integer, string) to macOS. Used by presets and Custom mode.
8-
- **Managed preferences first** — Tries to write to `/Library/Managed Preferences/com.brave.Browser.plist` so Brave enforces policies (Rewards, Wallet, etc. hidden). Falls back to user preferences (`~/Library/Preferences/com.brave.Browser.plist`) if the user cancels the auth dialog or lacks admin rights.
8+
- **Managed preferences first** — Tries to write to `/Library/Managed Preferences/com.brave.Browser.plist` (or `com.brave.Browser.beta.plist` with `--beta`) so Brave enforces policies (Rewards, Wallet, etc. hidden). Falls back to user preferences (`~/Library/Preferences/com.brave.Browser.plist` or `com.brave.Browser.beta.plist`) if the user cancels the auth dialog or lacks admin rights.
99
- **Raw XML plist for managed** — Managed plist is generated as valid XML (not via `defaults write`) and copied into place with correct ownership and permissions.
1010
- **Administrator privileges via AppleScript** — macOS authentication dialog (password or Touch ID) for writing to managed preferences; no password in the terminal.
1111
- **Reset** — Removes all Brave policy settings: deletes user plist keys (and the plist file when empty) and removes the managed plist when present. Returns whether a managed plist existed and whether it was removed. Reset is blocked if Brave is running (user is told to quit Brave first).
@@ -37,6 +37,7 @@ See [POLICY-ENFORCEMENT.md](POLICY-ENFORCEMENT.md) for the rationale and impleme
3737

3838
| Feature | Flags |
3939
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
40+
| Brave Beta | `--beta` — target Brave Browser Beta instead of stable (use with any command) |
4041
| Apply preset | `--apply`, `-a`, `--apply=<id>` (e.g. `max-privacy`, `balanced`) |
4142
| Privacy Guides | `--privacy-guides` (base from config or quick), `--privacy-guides=<base>` (e.g. max-privacy, custom) |
4243
| Apply from file | `--apply-file=<path>` (YAML with same `settings` format as presets) |

docs/INSTALL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ This guide covers installing Cowardly from a **release archive** (no Go or repo
55
## Prerequisites
66

77
- **macOS** (Intel or Apple Silicon)
8-
- **Brave Browser** installed at `/Applications/Brave Browser.app`
8+
- **Brave Browser** installed at `/Applications/Brave Browser.app` (or Brave Beta at `/Applications/Brave Browser Beta.app`; use `--beta` to target it)
99

1010
## Install with Homebrew
1111

docs/POLICY-ENFORCEMENT.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ Brave (like Chromium) supports policy keys that disable features such as Rewards
88

99
| Location | Path | Enforced? |
1010
| ----------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------- |
11-
| **User preferences** | `~/Library/Preferences/com.brave.Browser.plist` | **No** — Brave may still show Rewards, Wallet, etc. |
12-
| **Managed preferences** | `/Library/Managed Preferences/com.brave.Browser.plist` | **Yes** — Brave treats these as mandatory and hides/ disables the features |
11+
| **User preferences** | `~/Library/Preferences/com.brave.Browser.plist` (or `.beta` with `--beta`) | **No** — Brave may still show Rewards, Wallet, etc. |
12+
| **Managed preferences** | `/Library/Managed Preferences/com.brave.Browser.plist` (or `.beta` with `--beta`) | **Yes** — Brave treats these as mandatory and hides/ disables the features |
1313

1414
If you only run `defaults write com.brave.Browser BraveRewardsDisabled -bool true`, the key is written to the user plist. After restarting Brave, the UI can still show Rewards and Wallet. To get **enforced** behavior (features hidden/disabled), the same keys must be present in the **managed** plist under `/Library/Managed Preferences/`. That path is the standard macOS location for mandatory (MDM-style) policies.
1515

internal/brave/preferences.go

Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,7 @@ const plistXMLFooter = `
3131
</plist>
3232
`
3333

34-
const (
35-
// Domain is the macOS defaults domain for Brave Browser (user preferences).
36-
Domain = "com.brave.Browser"
37-
// ManagedPreferencesPath is the system path for mandatory policies (Brave enforces these and hides Rewards/Wallet etc.).
38-
ManagedPreferencesPath = "/Library/Managed Preferences/com.brave.Browser"
39-
)
34+
// Domain and ManagedPreferencesPath are now functions; see variant.go.
4035

4136
// ValueType is the plist value type for a preference.
4237
type ValueType string
@@ -153,7 +148,7 @@ func writeToPath(path string, s Setting) error {
153148

154149
// Write applies a single setting to user preferences using `defaults write`.
155150
func Write(s Setting) error {
156-
return writeToPath(Domain, s)
151+
return writeToPath(Domain(), s)
157152
}
158153

159154
// WriteAll applies multiple settings to user preferences; stops on first error.
@@ -191,7 +186,7 @@ func WriteAllToManaged(settings []Setting) error {
191186
}
192187
defer func() { _ = os.RemoveAll(tmpDir) }()
193188

194-
src := filepath.Join(tmpDir, "com.brave.Browser.plist")
189+
src := filepath.Join(tmpDir, Domain()+".plist")
195190
xmlContent := settingsToPlistXML(settings)
196191
if err := os.WriteFile(src, []byte(xmlContent), 0600); err != nil {
197192
return fmt.Errorf("write temp plist: %w", err)
@@ -201,8 +196,9 @@ func WriteAllToManaged(settings []Setting) error {
201196
// and never interpolated into the shell command, so there is no shell injection from presets.
202197
// Use AppleScript "with administrator privileges" so a GUI dialog appears (password or Touch ID).
203198
// chmod 644 so the plist is readable by Brave (per hi-one / managed preferences practice).
204-
shellCmd := fmt.Sprintf("mkdir -p \"/Library/Managed Preferences\" && cp %s \"/Library/Managed Preferences/com.brave.Browser.plist\" && chown root:wheel \"/Library/Managed Preferences/com.brave.Browser.plist\" && chmod 644 \"/Library/Managed Preferences/com.brave.Browser.plist\"",
205-
shellSingleQuoted(src))
199+
managedPlist := shellSingleQuoted(ManagedPreferencesPath() + ".plist")
200+
shellCmd := fmt.Sprintf("mkdir -p \"/Library/Managed Preferences\" && cp %s %s && chown root:wheel %s && chmod 644 %s",
201+
shellSingleQuoted(src), managedPlist, managedPlist, managedPlist)
206202
script := `do shell script "` + escapeForAppleScript(shellCmd) + `" with administrator privileges`
207203
ctx, cancel := context.WithTimeout(context.Background(), osascriptTimeout)
208204
defer cancel()
@@ -234,7 +230,7 @@ func Read(key string) (string, bool) {
234230
}
235231
ctx, cancel := context.WithTimeout(context.Background(), defaultsTimeout)
236232
defer cancel()
237-
cmd := exec.CommandContext(ctx, "defaults", "read", Domain, key)
233+
cmd := exec.CommandContext(ctx, "defaults", "read", Domain(), key)
238234
out, err := cmd.CombinedOutput()
239235
if err != nil || ctx.Err() != nil {
240236
return "", false
@@ -247,7 +243,7 @@ func ManagedPlistExists() bool {
247243
if !IsMacOS() {
248244
return false
249245
}
250-
_, err := os.Stat(ManagedPreferencesPath + ".plist")
246+
_, err := os.Stat(ManagedPreferencesPath() + ".plist")
251247
return err == nil
252248
}
253249

@@ -259,7 +255,7 @@ func ReadManaged(key string) (string, bool) {
259255
}
260256
ctx, cancel := context.WithTimeout(context.Background(), defaultsTimeout)
261257
defer cancel()
262-
cmd := exec.CommandContext(ctx, "defaults", "read", ManagedPreferencesPath, key)
258+
cmd := exec.CommandContext(ctx, "defaults", "read", ManagedPreferencesPath(), key)
263259
out, err := cmd.CombinedOutput()
264260
if err != nil || ctx.Err() != nil {
265261
return "", false
@@ -274,7 +270,7 @@ func Delete(key string) error {
274270
}
275271
ctx, cancel := context.WithTimeout(context.Background(), defaultsTimeout)
276272
defer cancel()
277-
cmd := exec.CommandContext(ctx, "defaults", "delete", Domain, key)
273+
cmd := exec.CommandContext(ctx, "defaults", "delete", Domain(), key)
278274
_ = cmd.Run() // ignore error if key missing
279275
return nil
280276
}
@@ -289,7 +285,7 @@ func Reset() (hadManaged, managedRemoved bool, err error) {
289285
}
290286
ctx, cancel := context.WithTimeout(context.Background(), defaultsTimeout)
291287
defer cancel()
292-
cmd := exec.CommandContext(ctx, "defaults", "delete", Domain)
288+
cmd := exec.CommandContext(ctx, "defaults", "delete", Domain())
293289
if out, runErr := cmd.CombinedOutput(); runErr != nil {
294290
outStr := string(out)
295291
// Domain already absent is fine (no user plist to delete).
@@ -303,7 +299,7 @@ func Reset() (hadManaged, managedRemoved bool, err error) {
303299
if userPath, pathErr := UserPreferencesPath(); pathErr == nil {
304300
_ = os.Remove(userPath)
305301
}
306-
managedPath := ManagedPreferencesPath + ".plist"
302+
managedPath := ManagedPreferencesPath() + ".plist"
307303
if _, statErr := os.Stat(managedPath); statErr == nil {
308304
hadManaged = true
309305
script := `do shell script "rm -f ` + escapeForAppleScript(shellSingleQuoted(managedPath)) + `" with administrator privileges`
@@ -317,15 +313,12 @@ func Reset() (hadManaged, managedRemoved bool, err error) {
317313
return hadManaged, managedRemoved, nil
318314
}
319315

320-
// BraveAppPath is the default path to the Brave Browser application.
321-
const BraveAppPath = "/Applications/Brave Browser.app"
322-
323-
// BraveInstalled checks if Brave Browser is installed at BraveAppPath.
316+
// BraveInstalled checks if Brave Browser is installed at BraveAppPath().
324317
func BraveInstalled() bool {
325318
if !IsMacOS() {
326319
return false
327320
}
328-
info, err := os.Stat(BraveAppPath)
321+
info, err := os.Stat(BraveAppPath())
329322
return err == nil && info.IsDir()
330323
}
331324

@@ -335,12 +328,12 @@ func BraveVersion() string {
335328
if !IsMacOS() {
336329
return ""
337330
}
338-
infoPlist := filepath.Join(BraveAppPath, "Contents", "Info.plist")
331+
infoPlist := filepath.Join(BraveAppPath(), "Contents", "Info.plist")
339332
if _, err := os.Stat(infoPlist); err != nil {
340333
return ""
341334
}
342335
// defaults read expects the path without .plist extension
343-
domain := filepath.Join(BraveAppPath, "Contents", "Info")
336+
domain := filepath.Join(BraveAppPath(), "Contents", "Info")
344337
ctx, cancel := context.WithTimeout(context.Background(), defaultsTimeout)
345338
defer cancel()
346339
cmd := exec.CommandContext(ctx, "defaults", "read", domain, "CFBundleShortVersionString")
@@ -359,7 +352,7 @@ func BraveRunning() bool {
359352
}
360353
ctx, cancel := context.WithTimeout(context.Background(), defaultsTimeout)
361354
defer cancel()
362-
cmd := exec.CommandContext(ctx, "pgrep", "-x", "Brave Browser")
355+
cmd := exec.CommandContext(ctx, "pgrep", "-x", braveProcessName())
363356
err := cmd.Run()
364357
return err == nil
365358
}
@@ -373,7 +366,7 @@ func UserPreferencesPath() (string, error) {
373366
if err != nil {
374367
return "", fmt.Errorf("home dir: %w", err)
375368
}
376-
return filepath.Join(home, "Library", "Preferences", Domain+".plist"), nil
369+
return filepath.Join(home, "Library", "Preferences", Domain()+".plist"), nil
377370
}
378371

379372
// BackupDir returns the backups directory path (~/Library/Application Support/cowardly/backups).

0 commit comments

Comments
 (0)