From 21632c5b6bb6d4cfe8fd6eeb0e188954d2a48e30 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Sun, 11 Oct 2026 15:31:57 -0400 Subject: [PATCH 1/2] Say that saved attachments are marked, and where that is best effort hey attachment save's help and docs/cli.md now say every saved file is marked as downloaded from the internet, that a macOS save that can't be marked fails, and that the Windows mark is written only where the drive can hold it. docs/tui.md stops promising the Windows mark everywhere and says a zip-based document can still open on Linux, in an archive manager at worst. Co-Authored-By: Claude Opus 5.5 --- docs/cli.md | 2 +- docs/tui.md | 2 +- internal/cmd/attachments_save.go | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 17b97bc04..3a2c12534 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -316,7 +316,7 @@ intact, including any existing signature or Name Tag. Use a body flag to replace it when needed. `draft show` includes the selected From address in both styled and JSON output. No command here persists a default sender. -`--attach` is repeatable on `hey compose`, `hey reply`, and `hey bulk-reply send`, and attachment-only messages are supported. The CLI validates and uploads every file before sending the email. `hey attachment list ` returns every named downloadable file, including named inline images. Direct files keep stable message-and-position IDs such as `456:1`; files inside embedded HTML receive opaque IDs scoped to their message. Pass either returned ID to `hey attachment save`. Saving uses the original filename by default, accepts `--output` for a file or directory, and preserves existing files unless `--force` is set. +`--attach` is repeatable on `hey compose`, `hey reply`, and `hey bulk-reply send`, and attachment-only messages are supported. The CLI validates and uploads every file before sending the email. `hey attachment list ` returns every named downloadable file, including named inline images. Direct files keep stable message-and-position IDs such as `456:1`; files inside embedded HTML receive opaque IDs scoped to their message. Pass either returned ID to `hey attachment save`. Saving uses the original filename by default, accepts `--output` for a file or directory, and preserves existing files unless `--force` is set. Every saved file is marked as downloaded from the internet, so the system warns before running it: the `com.apple.quarantine` attribute on macOS, where a save that cannot be marked fails rather than leaving an unmarked file, and a `Zone.Identifier` stream on Windows wherever the drive can hold one. Organization actions take the `id` values returned by `hey box view --json`, `hey label view --json`, or `hey search --json`. Reading, replying to, and forwarding a thread take its `topic_id` instead, which `hey box view --json`, `hey label view --json`, `hey collection view --json` and `hey search --json` all carry alongside `id`. `hey box view` also returns `next_page` and accepts `--page ` to continue a box listing; it keeps `next_history_url` for the sync clients that read it, and `--page` accepts that URL as readily as the cursor inside it. Label IDs come from `hey label list`; `hey label view` returns `next_page` and `total_count`, accepts `--page ` for continuation, and supports `--all` for complete traversal. `hey label create` files at least one thread as it creates the label, so it needs one or more of your box item IDs; a name you already use (in any case) is refused. diff --git a/docs/tui.md b/docs/tui.md index ae67ccf7b..fb3e7aa31 100644 --- a/docs/tui.md +++ b/docs/tui.md @@ -202,7 +202,7 @@ the iOS and Android apps each keep their own local choice too, and this is the s ## Attachments -Thread attachments always appear with their filename, media type, and size. Use `[` and `]` to select an attachment, `s` to save it without replacing an existing file, and Enter to download and open it in an external application — once Tab has selected a link, Enter opens the link instead, and Escape clears it. Enter only opens documents, images, audio and video (on Linux, only when the file's contents are one too, since the system picks the application from them); anything that could run a program, such as a script, a shortcut or a Terminal profile, has to be saved with `s` and opened by hand. Attachments never open automatically, and on macOS and Windows every saved attachment is marked as downloaded from the internet, so the system warns before running one. Kitty and Ghostty can show inline images. In the message itself, a file is marked by its kind: 📷 an image, 🎬 a video, 🎵 audio, 📄 a PDF or document, 📎 anything else. An image shows by name rather than by its URL, unless its name looks like a URL, when the real destination is shown beside it; an image on the web is a link you can select with Tab, and the row above the shortcut bar shows its whole destination. +Thread attachments always appear with their filename, media type, and size. Use `[` and `]` to select an attachment, `s` to save it without replacing an existing file, and Enter to download and open it in an external application — once Tab has selected a link, Enter opens the link instead, and Escape clears it. Enter only opens documents, images, audio and video (on Linux, only when the file's contents are one too, since the system picks the application from them; a zip-based document the system cannot tell from an archive still opens, in an archive manager at worst); anything that could run a program, such as a script, a shortcut or a Terminal profile, has to be saved with `s` and opened by hand. Attachments never open automatically. Every saved attachment is marked as downloaded from the internet, so the system warns before running one: always on macOS, and on Windows wherever the drive can hold the mark (a FAT drive cannot). Kitty and Ghostty can show inline images. In the message itself, a file is marked by its kind: 📷 an image, 🎬 a video, 🎵 audio, 📄 a PDF or document, 📎 anything else. An image shows by name rather than by its URL, unless its name looks like a URL, when the real destination is shown beside it; an image on the web is a link you can select with Tab, and the row above the shortcut bar shows its whole destination. ## Contacts diff --git a/internal/cmd/attachments_save.go b/internal/cmd/attachments_save.go index aea7e2f7d..1e2555bf4 100644 --- a/internal/cmd/attachments_save.go +++ b/internal/cmd/attachments_save.go @@ -33,8 +33,11 @@ func newAttachmentsSaveCommand() *attachmentsSaveCommand { attachmentsSaveCommand.cmd = &cobra.Command{ Use: "save ", Short: "Save an attachment to disk", + Long: "Save an attachment to disk under its original filename, or at --output. Existing files are preserved unless --force is set. " + + "The saved file is marked as downloaded from the internet, so the system warns before running it: on macOS a save that cannot be marked fails, " + + "and on Windows the mark is written wherever the drive can hold it.", Annotations: map[string]string{ - "agent_notes": "Saves the attachment identified by an ID from `hey attachment list `. Existing files are preserved unless --force is set.", + "agent_notes": "Saves the attachment identified by an ID from `hey attachment list `. Existing files are preserved unless --force is set. The saved file is marked as downloaded from the internet (macOS quarantine, Windows Zone.Identifier); on macOS a save that cannot be marked fails.", }, Example: ` hey attachment save 67890:1 hey attachment save 67890:1 --output ./quarterly-report.pdf From 049ef900cfd7eb4cb033619d931341af98db0e07 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Sun, 11 Oct 2026 16:10:52 -0400 Subject: [PATCH 2/2] Say the download mark is macOS and Windows only Linux and the other systems keep no record of where a file came from, so nothing marks a file saved there; the help text and docs said every saved file was marked. Co-Authored-By: Claude Opus 5.5 --- docs/cli.md | 2 +- docs/tui.md | 2 +- internal/cmd/attachments_save.go | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 3a2c12534..1bd714132 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -316,7 +316,7 @@ intact, including any existing signature or Name Tag. Use a body flag to replace it when needed. `draft show` includes the selected From address in both styled and JSON output. No command here persists a default sender. -`--attach` is repeatable on `hey compose`, `hey reply`, and `hey bulk-reply send`, and attachment-only messages are supported. The CLI validates and uploads every file before sending the email. `hey attachment list ` returns every named downloadable file, including named inline images. Direct files keep stable message-and-position IDs such as `456:1`; files inside embedded HTML receive opaque IDs scoped to their message. Pass either returned ID to `hey attachment save`. Saving uses the original filename by default, accepts `--output` for a file or directory, and preserves existing files unless `--force` is set. Every saved file is marked as downloaded from the internet, so the system warns before running it: the `com.apple.quarantine` attribute on macOS, where a save that cannot be marked fails rather than leaving an unmarked file, and a `Zone.Identifier` stream on Windows wherever the drive can hold one. +`--attach` is repeatable on `hey compose`, `hey reply`, and `hey bulk-reply send`, and attachment-only messages are supported. The CLI validates and uploads every file before sending the email. `hey attachment list ` returns every named downloadable file, including named inline images. Direct files keep stable message-and-position IDs such as `456:1`; files inside embedded HTML receive opaque IDs scoped to their message. Pass either returned ID to `hey attachment save`. Saving uses the original filename by default, accepts `--output` for a file or directory, and preserves existing files unless `--force` is set. On macOS and Windows a saved file is marked as downloaded from the internet, so the system warns before running it: the `com.apple.quarantine` attribute on macOS, where a save that cannot be marked fails rather than leaving an unmarked file, and, as a best effort, a `Zone.Identifier` stream on Windows wherever the drive can hold one. Other systems keep no such mark, so files saved there are not marked. Organization actions take the `id` values returned by `hey box view --json`, `hey label view --json`, or `hey search --json`. Reading, replying to, and forwarding a thread take its `topic_id` instead, which `hey box view --json`, `hey label view --json`, `hey collection view --json` and `hey search --json` all carry alongside `id`. `hey box view` also returns `next_page` and accepts `--page ` to continue a box listing; it keeps `next_history_url` for the sync clients that read it, and `--page` accepts that URL as readily as the cursor inside it. Label IDs come from `hey label list`; `hey label view` returns `next_page` and `total_count`, accepts `--page ` for continuation, and supports `--all` for complete traversal. `hey label create` files at least one thread as it creates the label, so it needs one or more of your box item IDs; a name you already use (in any case) is refused. diff --git a/docs/tui.md b/docs/tui.md index fb3e7aa31..0a5685f31 100644 --- a/docs/tui.md +++ b/docs/tui.md @@ -202,7 +202,7 @@ the iOS and Android apps each keep their own local choice too, and this is the s ## Attachments -Thread attachments always appear with their filename, media type, and size. Use `[` and `]` to select an attachment, `s` to save it without replacing an existing file, and Enter to download and open it in an external application — once Tab has selected a link, Enter opens the link instead, and Escape clears it. Enter only opens documents, images, audio and video (on Linux, only when the file's contents are one too, since the system picks the application from them; a zip-based document the system cannot tell from an archive still opens, in an archive manager at worst); anything that could run a program, such as a script, a shortcut or a Terminal profile, has to be saved with `s` and opened by hand. Attachments never open automatically. Every saved attachment is marked as downloaded from the internet, so the system warns before running one: always on macOS, and on Windows wherever the drive can hold the mark (a FAT drive cannot). Kitty and Ghostty can show inline images. In the message itself, a file is marked by its kind: 📷 an image, 🎬 a video, 🎵 audio, 📄 a PDF or document, 📎 anything else. An image shows by name rather than by its URL, unless its name looks like a URL, when the real destination is shown beside it; an image on the web is a link you can select with Tab, and the row above the shortcut bar shows its whole destination. +Thread attachments always appear with their filename, media type, and size. Use `[` and `]` to select an attachment, `s` to save it without replacing an existing file, and Enter to download and open it in an external application — once Tab has selected a link, Enter opens the link instead, and Escape clears it. Enter only opens documents, images, audio and video (on Linux, only when the file's contents are one too, since the system picks the application from them; a zip-based document the system cannot tell from an archive still opens, in an archive manager at worst); anything that could run a program, such as a script, a shortcut or a Terminal profile, has to be saved with `s` and opened by hand. Attachments never open automatically. On macOS and Windows every saved attachment is marked as downloaded from the internet, so the system warns before running one: always on macOS, and on Windows wherever the drive can hold the mark (a FAT drive cannot). Linux keeps no such mark. Kitty and Ghostty can show inline images. In the message itself, a file is marked by its kind: 📷 an image, 🎬 a video, 🎵 audio, 📄 a PDF or document, 📎 anything else. An image shows by name rather than by its URL, unless its name looks like a URL, when the real destination is shown beside it; an image on the web is a link you can select with Tab, and the row above the shortcut bar shows its whole destination. ## Contacts diff --git a/internal/cmd/attachments_save.go b/internal/cmd/attachments_save.go index 1e2555bf4..a40d165bc 100644 --- a/internal/cmd/attachments_save.go +++ b/internal/cmd/attachments_save.go @@ -34,10 +34,11 @@ func newAttachmentsSaveCommand() *attachmentsSaveCommand { Use: "save ", Short: "Save an attachment to disk", Long: "Save an attachment to disk under its original filename, or at --output. Existing files are preserved unless --force is set. " + - "The saved file is marked as downloaded from the internet, so the system warns before running it: on macOS a save that cannot be marked fails, " + - "and on Windows the mark is written wherever the drive can hold it.", + "On macOS and Windows the saved file is marked as downloaded from the internet, so the system warns before running it: " + + "on macOS a save that cannot be marked fails, and on Windows the mark is a best effort, written wherever the drive can hold it. " + + "Other systems keep no such mark.", Annotations: map[string]string{ - "agent_notes": "Saves the attachment identified by an ID from `hey attachment list `. Existing files are preserved unless --force is set. The saved file is marked as downloaded from the internet (macOS quarantine, Windows Zone.Identifier); on macOS a save that cannot be marked fails.", + "agent_notes": "Saves the attachment identified by an ID from `hey attachment list `. Existing files are preserved unless --force is set. On macOS the saved file gets the quarantine attribute, and a save that cannot be marked fails; on Windows a Zone.Identifier mark is written as a best effort, and may be missing on drives that cannot hold it; other platforms add no mark.", }, Example: ` hey attachment save 67890:1 hey attachment save 67890:1 --output ./quarterly-report.pdf