show the triggering brew command in the sudo password dialog

The askpass script is now rewritten before each brew invocation with
the current command baked into the dialog text ("...to run: brew
upgrade tailscale-app") instead of a generic message written once at
launch. The command text is whitelisted to characters that cannot
break out of the shell/AppleScript quoting it is embedded in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
maxsoch 2026-07-06 22:41:42 +02:00
parent 421ae5c40d
commit 89921275b8

View file

@ -145,22 +145,29 @@ class AppDelegate: NSObject, NSApplicationDelegate {
return "brew" return "brew"
} }
/// Written once per launch; sudo runs it to show a GUI password dialog /// Rewritten before each brew command so the sudo password dialog can say
/// whenever brew needs admin rights (no terminal is attached to Process). /// which command needs it. Sudo runs this script to show a GUI password
static let askpassURL: URL = { /// prompt whenever brew needs admin rights (no terminal is attached to
/// Process). If two brew commands overlap, the later one's text wins
/// harmless, since prompts realistically only appear during upgrades.
static func writeAskpassScript(for command: String) -> URL {
let dir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] let dir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("BrewBar", isDirectory: true) .appendingPathComponent("BrewBar", isDirectory: true)
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
// the text lands inside shell single quotes AND an AppleScript string;
// whitelist characters that cannot break out of either
let safeCommand = command.filter { $0.isLetter || $0.isNumber || " ._@+=:/-".contains($0) }
let url = dir.appendingPathComponent("askpass.sh") let url = dir.appendingPathComponent("askpass.sh")
let script = """ let script = """
#!/bin/zsh #!/bin/zsh
osascript -e 'display dialog "BrewBar needs your administrator password to continue:" default answer "" with hidden answer with title "BrewBar" with icon caution buttons {"Cancel", "OK"} default button "OK"' -e 'text returned of result' osascript -e 'display dialog "BrewBar needs your administrator password to run:\\n\\nbrew \(safeCommand)" default answer "" with hidden answer with title "BrewBar" with icon caution buttons {"Cancel", "OK"} default button "OK"' -e 'text returned of result'
""" """
try? script.write(to: url, atomically: true, encoding: .utf8) try? script.write(to: url, atomically: true, encoding: .utf8)
try? FileManager.default.setAttributes([.posixPermissions: 0o700], ofItemAtPath: url.path) try? FileManager.default.setAttributes([.posixPermissions: 0o700], ofItemAtPath: url.path)
return url return url
}() }
func runBrew(_ command: String, completion: @escaping (String) -> Void = { _ in }) { func runBrew(_ command: String, completion: @escaping (String) -> Void = { _ in }) {
DispatchQueue.global().async { DispatchQueue.global().async {
@ -177,7 +184,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
process.arguments = arguments process.arguments = arguments
var environment = ProcessInfo.processInfo.environment var environment = ProcessInfo.processInfo.environment
environment["SUDO_ASKPASS"] = Self.askpassURL.path environment["SUDO_ASKPASS"] = Self.writeAskpassScript(for: cleaned).path
process.environment = environment process.environment = environment
let pipe = Pipe() let pipe = Pipe()