From 8ca01b9d49eed312858ca4f36e5f2416f4edc280 Mon Sep 17 00:00:00 2001 From: Umputun Date: Thu, 20 Aug 2026 16:09:58 -0500 Subject: [PATCH 1/3] fix: explain why agterm asks for Desktop, Documents and Downloads macOS defines an optional usage string per protected folder and falls back to its own copy when none is present, so the user was asked why a terminal wants his Downloads and told nothing. The other sixteen strings in this file all explain hosted-CLI responsibility; these five now say the same thing in the same voice. They gate nothing. agterm can obtain these grants without them, so this repairs no denial and adds no access, it only replaces Apple's generic prompt copy with the reason. CI pins the entitlement set and asserts nothing here, and confirming the text renders needs a signed build with fresh TCC state by hand. --- agterm/Info.plist | 10 ++++++++ .../no-folder-usage-strings-in-info-plist.md | 25 ------------------- 2 files changed, 10 insertions(+), 25 deletions(-) delete mode 100644 docs/backlog/no-folder-usage-strings-in-info-plist.md diff --git a/agterm/Info.plist b/agterm/Info.plist index 27397a45..17d851ce 100644 --- a/agterm/Info.plist +++ b/agterm/Info.plist @@ -50,18 +50,28 @@ Command-line tools you run inside agterm (for example, ffmpeg capturing video) request camera access through agterm. NSContactsUsageDescription Command-line tools you run inside agterm request access to your contacts through agterm. + NSDesktopFolderUsageDescription + Command-line tools you run inside agterm request access to your Desktop folder through agterm. + NSDocumentsFolderUsageDescription + Command-line tools you run inside agterm request access to your Documents folder through agterm. + NSDownloadsFolderUsageDescription + Command-line tools you run inside agterm request access to your Downloads folder through agterm. NSLocalNetworkUsageDescription Command-line tools you run inside agterm (for example, connecting to a .local host) request local network access through agterm. NSLocationUsageDescription Command-line tools you run inside agterm request your location through agterm. NSMicrophoneUsageDescription Command-line tools you run inside agterm (for example, voice dictation in Claude Code) request microphone access through agterm. + NSNetworkVolumesUsageDescription + Command-line tools you run inside agterm request access to files on network volumes through agterm. NSPhotoLibraryUsageDescription Command-line tools you run inside agterm (for example, osxphotos) request access to your photo library through agterm. NSRemindersFullAccessUsageDescription Command-line tools you run inside agterm request access to your reminders through agterm. NSRemindersUsageDescription Command-line tools you run inside agterm request access to your reminders through agterm. + NSRemovableVolumesUsageDescription + Command-line tools you run inside agterm request access to files on removable volumes through agterm. NSSpeechRecognitionUsageDescription Command-line tools you run inside agterm request speech recognition through agterm. NSSystemAdministrationUsageDescription diff --git a/docs/backlog/no-folder-usage-strings-in-info-plist.md b/docs/backlog/no-folder-usage-strings-in-info-plist.md deleted file mode 100644 index 73d8130c..00000000 --- a/docs/backlog/no-folder-usage-strings-in-info-plist.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -worth: later -where: agterm/Info.plist:37 -added: 2026-08-20 ---- -# Info.plist declares no usage string for the protected folders - -The sixteen `NSxxxUsageDescription` strings cover the entitlement-gated services and none of the -Files & Folders family. macOS defines an optional string per protected folder — -`NSDesktopFolderUsageDescription`, `NSDocumentsFolderUsageDescription`, -`NSDownloadsFolderUsageDescription`, `NSNetworkVolumesUsageDescription`, -`NSRemovableVolumesUsageDescription` — and with none present it falls back to its own copy, measured in -`TCC.framework/Versions/A/Resources/Localizable.loctable` as -`REQUEST_ACCESS_SERVICE_kTCCServiceSystemPolicyDownloadsFolder` = `“%@” would like to access -files in your Downloads folder.` So the user is asked why agterm wants his Downloads and told nothing. - -Every other family already explains hosted-CLI responsibility in its string, so the same pattern applies: -"Command-line tools you run inside agterm may request access to your Downloads folder through agterm." -WezTerm ships two of these written for exactly that case; Ghostty, kitty and iTerm2 ship none. - -Deferred because it fixes nothing. The strings are not a gate — agterm can obtain these grants today -without them — so this repairs no denial and adds no access, and the only change is five pieces of prompt -copy. It also cannot be proven by CI, which pins the entitlement set and asserts nothing about -`Info.plist`: confirming the custom text actually renders needs a signed build and a fresh TCC state by -hand. Surfaced investigating #468, where the missing strings turned out not to be the cause. From 8ed52d223c57a7b1238c1744c0801aec7e255112 Mon Sep 17 00:00:00 2001 From: Umputun Date: Thu, 20 Aug 2026 16:10:05 -0500 Subject: [PATCH 2/3] fix(keymap): pin the seeded Lazygit example to its own session `--target` defaults to `active`, and `active` resolves when the request reaches the server rather than when the chord captured its context. Custom commands are spawned detached and fire-and-forget, so a session or window switch between the keypress and delivery opened the overlay over whatever was selected by then. `$AGT_SESSION_ID` is the stable id a custom command already has for this. The seed is now what a new user reads next to the pinned example in the command reference, so the two no longer give different answers. The neighbouring Zed and Deploy examples are unaffected: neither sends a session-addressed request. --- .../Sources/agtermCore/ConfigPaths.swift | 2 +- .../agtermCoreTests/ConfigPathsTests.swift | 2 +- .../seeded-lazygit-example-omits-target.md | 29 ------------------- 3 files changed, 2 insertions(+), 31 deletions(-) delete mode 100644 docs/backlog/seeded-lazygit-example-omits-target.md diff --git a/agtermCore/Sources/agtermCore/ConfigPaths.swift b/agtermCore/Sources/agtermCore/ConfigPaths.swift index 6a8d16c8..9040e125 100644 --- a/agtermCore/Sources/agtermCore/ConfigPaths.swift +++ b/agtermCore/Sources/agtermCore/ConfigPaths.swift @@ -74,7 +74,7 @@ public enum ConfigPaths { # Examples: # # command "Open in Zed" cmd+shift+e open -a Zed "$AGT_SESSION_PWD" - # command "Lazygit" ctrl+a>g agtermctl session overlay open 'zsh -lc lazygit' --socket "$AGT_SOCKET" + # command "Lazygit" ctrl+a>g agtermctl session overlay open 'zsh -lc lazygit' --target "$AGT_SESSION_ID" --socket "$AGT_SOCKET" # command "Deploy" ./deploy.sh # # global-hotkey diff --git a/agtermCore/Tests/agtermCoreTests/ConfigPathsTests.swift b/agtermCore/Tests/agtermCoreTests/ConfigPathsTests.swift index df070b8d..55466bef 100644 --- a/agtermCore/Tests/agtermCoreTests/ConfigPathsTests.swift +++ b/agtermCore/Tests/agtermCoreTests/ConfigPathsTests.swift @@ -37,7 +37,7 @@ struct ConfigPathsTests { #expect(starter.contains("command \"\" [chord] ")) #expect(starter.contains("single chord OR a leader like `ctrl+a>g`")) #expect(starter.contains("command \"Open in Zed\" cmd+shift+e open -a Zed \"$AGT_SESSION_PWD\"")) - #expect(starter.contains("command \"Lazygit\" ctrl+a>g agtermctl session overlay open 'zsh -lc lazygit' --socket \"$AGT_SOCKET\"")) + #expect(starter.contains("command \"Lazygit\" ctrl+a>g agtermctl session overlay open 'zsh -lc lazygit' --target \"$AGT_SESSION_ID\" --socket \"$AGT_SOCKET\"")) #expect(starter.contains("command \"Deploy\" ./deploy.sh")) #expect(starter.contains("ctrl+shift+p")) #expect(!starter.contains("super")) diff --git a/docs/backlog/seeded-lazygit-example-omits-target.md b/docs/backlog/seeded-lazygit-example-omits-target.md deleted file mode 100644 index 286e563e..00000000 --- a/docs/backlog/seeded-lazygit-example-omits-target.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -worth: later -where: agtermCore/Sources/agtermCore/ConfigPaths.swift:77 -added: 2026-08-20 ---- -# the seeded Lazygit example omits --target and can open its overlay elsewhere - -The starter `keymap.conf` ships: - -``` -command "Lazygit" ctrl+a>g agtermctl session overlay open 'zsh -lc lazygit' --socket "$AGT_SOCKET" -``` - -`TargetOptions.target` defaults to `active` (`agtermCore/Sources/agtermctlKit/Commands.swift:69`), and `active` -is resolved when the request reaches the server, not when the chord built its context. The custom command is -spawned detached and fire-and-forget (`agterm/Commands/CustomCommandRunner.swift:336`), so a session or window -switch between the keypress and delivery opens the overlay over whatever is selected by then. The command -reference already tells automated callers to pin it (`site/commands.html:379`), and a custom command has the -stable `$AGT_SESSION_ID` for exactly this. - -The fix is one flag: `--target "$AGT_SESSION_ID"`. The line is otherwise valid — `overlay open` does not -require a size flag, so omitting `--size-percent` correctly gives a full-size overlay. - -Worth doing because the `#extend` lesson now sends new users to **File ▸ Edit Keymap…**, where they read this -example next to the docs' pinned one and get two different answers. Kept out of the docs change (#PR) because -touching `ConfigPaths.swift` pulls in the full Swift gate run for a one-line seed-text improvement, and no -existing user is blocked by it. - -Found by codex while reviewing the paste lines in the `#extend` lesson. From ca75136d134d95ebb49324627cfb4039d30b42fc Mon Sep 17 00:00:00 2001 From: Umputun Date: Thu, 20 Aug 2026 17:52:21 -0500 Subject: [PATCH 3/3] refactor(hooks): stop exporting the enum's unused public surface `codexWrapperPath` was public with its only caller in the same file, sitting beside `codexWrapperName`, which earns its public through `agterm/AgentHooksInstaller.swift:136`. The two looked alike while only one was an app-target contract, so a reader had to grep to tell them apart. The backlog item asked for the rest of the enum in the same pass rather than one symbol alone, and eight more were the same: piExtensionName, opencodePluginName, rcMarkerBegin, rcMarkerEnd, piExtensionDirectory, opencodePluginDirectory, wrapperPath and codexHooksBlock. Checked the app target, agtermctlKit and agtermctl for each; the only consumers are this file and the tests, and a testable import needs no public. What stays exported is what the app target actually calls, so the surface now answers "app-target contract" rather than "happened to be written public". The app target builds against the narrowed interface. --- .../agtermCore/AgentHooksInstall.swift | 18 +++++++++--------- ...wrapper-path-is-public-with-no-consumer.md | 19 ------------------- 2 files changed, 9 insertions(+), 28 deletions(-) delete mode 100644 docs/backlog/codex-wrapper-path-is-public-with-no-consumer.md diff --git a/agtermCore/Sources/agtermCore/AgentHooksInstall.swift b/agtermCore/Sources/agtermCore/AgentHooksInstall.swift index 3dc8bbf3..97f6a797 100644 --- a/agtermCore/Sources/agtermCore/AgentHooksInstall.swift +++ b/agtermCore/Sources/agtermCore/AgentHooksInstall.swift @@ -15,7 +15,7 @@ public enum AgentHooksInstall { /// The bundled Pi extension's path relative to the agent-status package, and its destination filename. public static let piExtensionRelativePath = "pi/agterm-status.ts" - public static let piExtensionName = "agterm-status.ts" + static let piExtensionName = "agterm-status.ts" /// Ownership sentinel in the bundled Pi extension: a reinstall refuses to overwrite an unmarked same-named /// extension, preserving a user-authored integration. @@ -23,7 +23,7 @@ public enum AgentHooksInstall { /// The bundled OpenCode plugin's path relative to the agent-status package, and its destination filename. public static let opencodePluginRelativePath = "opencode/agterm-status.js" - public static let opencodePluginName = "agterm-status.js" + static let opencodePluginName = "agterm-status.js" /// Ownership sentinel in the bundled OpenCode plugin, same policy as `piExtensionMarker`. Named `*Plugin*` /// (not `*Extension*`) because OpenCode's host term is plugin — a deliberate divergence from `piExtension*`. @@ -36,8 +36,8 @@ public enum AgentHooksInstall { /// Marker lines bracketing the agterm-managed block in a shell rc file; the opening marker is also the /// idempotency probe (present → already installed). - public static let rcMarkerBegin = "# >>> agterm agent-status >>>" - public static let rcMarkerEnd = "# <<< agterm agent-status <<<" + static let rcMarkerBegin = "# >>> agterm agent-status >>>" + static let rcMarkerEnd = "# <<< agterm agent-status <<<" /// The Claude Code hook events the merge installs, paired with the state (plus flags) each maps to. /// `UserPromptSubmit` and `PostToolUse` both set `active` — the latter after every tool run, so the status @@ -64,7 +64,7 @@ public enum AgentHooksInstall { ] /// The destination directory for Pi's auto-discovered global extensions. - public static func piExtensionDirectory(home: String) -> String { + static func piExtensionDirectory(home: String) -> String { home + "/.pi/agent/extensions" } @@ -81,7 +81,7 @@ public enum AgentHooksInstall { } /// The destination directory for OpenCode's auto-discovered global plugins. - public static func opencodePluginDirectory(home: String) -> String { + static func opencodePluginDirectory(home: String) -> String { home + "/.config/opencode/plugins" } @@ -286,11 +286,11 @@ public enum AgentHooksInstall { /// the absolute wrapper-script path the installed hooks invoke (`/agterm-agent-status.sh`); the /// caller's hook entry appends the state. - public static func wrapperPath(scriptDir: String) -> String { + static func wrapperPath(scriptDir: String) -> String { scriptDir + "/" + wrapperName } - public static func codexWrapperPath(scriptDir: String) -> String { + static func codexWrapperPath(scriptDir: String) -> String { scriptDir + "/" + codexWrapperName } @@ -299,7 +299,7 @@ public enum AgentHooksInstall { /// merge declines, and nothing checks the two against each other. /// The wrapper's absolute path is baked into each command — shell-quoted (so a path with spaces stays one /// token) inside a TOML basic string — so the hook fires without the CLI on PATH. - public static func codexHooksBlock(scriptDir: String) -> String { + static func codexHooksBlock(scriptDir: String) -> String { let wrapper = shellQuote(codexWrapperPath(scriptDir: scriptDir)) return codexHooks.map { hook in """ diff --git a/docs/backlog/codex-wrapper-path-is-public-with-no-consumer.md b/docs/backlog/codex-wrapper-path-is-public-with-no-consumer.md deleted file mode 100644 index 98603c98..00000000 --- a/docs/backlog/codex-wrapper-path-is-public-with-no-consumer.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -worth: later -where: agtermCore/Sources/agtermCore/AgentHooksInstall.swift:293 -added: 2026-08-19 ---- -# codexWrapperPath is public with nothing outside the module calling it - -`codexWrapperPath(scriptDir:)` is `public` but its only caller is `codexHooksBlock` at `:303`, in the same -file. `internal` would serve it. Contrast `codexWrapperName` two lines up, whose `public` is earned by -`agterm/AgentHooksInstaller.swift:136`. - -Against the standing rule that visibility is private by default and exported only when used outside the -package. Nothing is broken; the cost is that a reader has to grep to learn which of the two neighbouring -symbols is an app-target contract and which is not. - -Worth doing whenever something else touches this file, and worth checking the rest of the enum's public -surface in the same pass rather than tightening one symbol alone. - -Surfaced reviewing PR #461.