Skip to content
Merged
4 changes: 4 additions & 0 deletions .surface
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,11 @@ hey search --to
hey search filters
hey seen
hey setup
hey setup --silent-success
hey setup --skip-agents
hey setup --skip-omarchy
hey setup agents
hey setup agents --remove
hey setup claude
hey setup codex
hey setup omarchy
Expand Down
33 changes: 18 additions & 15 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,28 +52,31 @@ login`, `hey auth logout`, `hey auth status`), the `hey login`/`hey logout` shor
`hey setup` and `hey doctor` work without authentication.

Bare `hey` at an interactive terminal runs the setup wizard when logged out
(`runSetupWizard` in `internal/cmd/setup.go` — welcome, OAuth sign-in, linked-account
greeting, coding-agent setup, summary) and stops at the summary; in every other case it
(`runSetupWizard` in `internal/cmd/setup.go` — welcome, OAuth sign-in, signed-in identity,
coding-agent setup, summary) and stops at the summary; `hey setup --skip-agents`
and `--skip-omarchy` leave those integrations unchanged. In every other case bare `hey`
prints help. The TUI lives at `hey tui` (plus the hidden `hey hey`). `config.json`'s
`onboarded` flag only trims a later logged-out run to the sign-in step. `HEY_NONINTERACTIVE=1`
disables every prompt regardless of TTY detection: the wizard skips OAuth and answers its
own confirmations with their defaults.

On Omarchy, an interactive OAuth sign-in — `hey auth login`/`hey login`, the wizard, or
`requireAuth`'s prompt — offers to install the `37signals.hey` bar plugin
(`internal/cmd/omarchy_plugin.go`): asked once, remembered in
`StateDir()/omarchy/bar-plugin.json`, serialized by a flock next to it. The hook never
runs for machine output, `HEY_NONINTERACTIVE`, a non-TTY, or `--token`/`--cookie` logins,
and it never re-enables a plugin that is off the bar — only explicit `hey setup omarchy`
does, which installs in every output format and fails loudly (`setup_failed`) on any
incomplete outcome; `--remove` writes its tombstone first, disables, and keeps the
checkout. Details and the state model are in docs/omarchy.md.
disables interactive sign-in regardless of TTY detection; detected agent setup continues
without prompting.

On Omarchy, the full `hey setup` wizard installs and enables the `37signals.hey` bar plugin
without prompting. Other interactive OAuth sign-ins — `hey auth login`/`hey login`, the
lite wizard, or `requireAuth`'s prompt — offer it once and remember the answer in
`StateDir()/omarchy/bar-plugin.json`, serialized by a flock next to it. The sign-in hook
never runs for machine output, `HEY_NONINTERACTIVE`, a non-TTY, or `--token`/`--cookie`
logins, and it never re-enables a plugin that is off the bar. The full wizard and explicit
`hey setup omarchy` do re-enable it; the explicit command installs in every output format
and fails loudly (`setup_failed`) on any incomplete outcome. `--remove` writes its
tombstone first, disables, and keeps the checkout. Details and the state model are in
docs/omarchy.md.

Coding-agent integration lives in `internal/harness` (agent registry, Claude Code / Codex
detection, plugin and skill health checks) and `internal/cmd/setup_agent*.go` (`hey setup
claude|codex|agents`). Claude Code gets the `hey@37signals` plugin from `basecamp/claude-plugins`
plus a skill link; Codex gets the skill only until a `.codex-plugin` ships. `HEY_SETUP_AGENT`
selects the target for `hey setup agents`. `hey doctor` reports per-agent diagnostics, and a
selects the target for `hey setup agents`; `hey setup agents --remove` uninstalls the
Claude plugin and removes only hey-cli-managed skill files. `hey doctor` reports per-agent diagnostics, and a
`PersistentPostRunE` hook (`skill_refresh.go`) re-syncs installed skill copies once per release
version change. Ownership is explicit: every skill write goes through `claimSkillDir`, which
marks a directory it creates (`.managed-by-hey-cli`) and refuses a populated one without the
Expand Down
71 changes: 49 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,27 @@ A CLI and TUI for [HEY](https://hey.com).

## Install

**Omarchy**

```bash
omarchy-mise-install github:basecamp/hey-cli hey
```

**macOS / Linux / WSL2**

```bash
curl -fsSL https://hey.com/install-cli | bash
```

Or with mise:

```bash
MISE_MINIMUM_RELEASE_AGE=0 mise use -g github:basecamp/hey-cli
```

The override selects the current release immediately; mise otherwise waits for a new
release to be 24 hours old.

**Windows (PowerShell)**

```powershell
Expand All @@ -46,8 +61,12 @@ hey
```

The first time you run `hey` at a terminal it walks you through setup: it signs you in
(browser-based OAuth), shows the mail accounts linked to your HEY identity, and offers to
connect your coding agents (Claude Code, Codex). After that, `hey tui` opens the app and
with browser-based OAuth, confirms your identity, and connects your detected coding agents
(Claude Code, Codex). Run `hey setup --skip-agents` to leave
agent integrations unchanged, or add `--skip-omarchy` to leave Omarchy unchanged.
`hey setup --silent-success` keeps required authentication visible, shows an installation
spinner, and ends with `SETUP COMPLETE`; failure guidance remains visible. After that,
`hey tui` opens the app and
bare `hey` prints the help. `hey setup` reruns the wizard any time; `hey login` and
`hey logout` are shortcuts for `hey auth login` and `hey auth logout`.

Expand All @@ -60,12 +79,17 @@ Both scripts download the release for your platform, verify its SHA-256 checksum
<details>
<summary>Other installation methods</summary>

**mise:**
```bash
MISE_MINIMUM_RELEASE_AGE=0 mise use -g github:basecamp/hey-cli
```

**Homebrew (macOS / Linux):**
```bash
brew install --cask basecamp/tap/hey
```

**Arch Linux / Omarchy (AUR):**
**Arch Linux (AUR):**
```bash
yay -S hey-cli
```
Expand Down Expand Up @@ -138,12 +162,13 @@ hey upgrade 0.2.0-rc.1 # target a specific release, e.g. a prerelease
```

Upgrading only ever moves forward: a requested version at or below the installed one is a
no-op, and package-manager installs always follow their manager's own version (a pinned
version is refused there).
no-op. Mise and native installs can select a specific version; Homebrew and Scoop always
follow their manager's own version and refuse a pinned upgrade.

What happens depends on how hey was installed:

- **Installer script / tarball** (a binary under your home directory, e.g. `~/.local/bin` or `~/bin`): upgrades in place. hey downloads the release for your platform, verifies its Sigstore signature (the keyless `checksums.txt.bundle` published by the release pipeline, identity-pinned to the release workflow and tag) and SHA-256 checksum, swaps the executable transactionally, and confirms the installed binary reports the new version. On failure the previous binary is restored; in the worst case — restoration itself fails mid-swap — the error names the preserved backup file next to the binary so you can put it back by hand.
- **mise**: delegates to mise, updates the configuration that selected the running binary, then verifies normal mise resolution selects the new version.
- **Homebrew / Scoop**: delegates to `brew upgrade --cask basecamp/tap/hey` / `scoop update hey`, then verifies the manager-installed binary actually reports the new version.
- **System packages** (apt/dnf/apk, AUR, Nix) and **`go install` builds**: never touched. `hey upgrade` exits nonzero with upgrade guidance for that install method (the exact command where it can be known, e.g. `go install` or `yay -S hey-cli`; otherwise which package manager to use).

Expand Down Expand Up @@ -663,9 +688,9 @@ read from `~/.local/state/omarchy/current/theme/`, and restyles live when you ru
— an explicitly chosen file is trusted as written — or `NO_COLOR=1` to turn color off.

```bash
omarchy pkg aur add hey-cli # hey-cli is on the AUR
hey auth login # signing in with hey puts HEY in your bar (asked once)
hey setup omarchy # the explicit path: installs in every output format, fails loudly
omarchy-mise-install github:basecamp/hey-cli hey
hey setup # sign in, connect detected agents, and put HEY in your bar
hey setup omarchy # the explicit desktop-only path: installs in every output format, fails loudly
hey setup omarchy --notify # toast new mail from the bar plugin (--no-notify turns it off)
hey setup omarchy --remove # disable the bar plugin (checkout kept) and remove the desktop pieces
```
Expand All @@ -676,13 +701,14 @@ its engine — the plugin reads the Imbox with `hey box view imbox` and runs `he
bar is live: a thread you archive in the TUI, on your phone or in the web app leaves the
panel within a second, and after a disconnect the watch catches up from where it left off.

Signing in interactively — `hey auth login`, the setup wizard, or any command's sign-in
prompt — offers to install that plugin, asks once, and remembers your answer. The offer
never fires in scripts: machine output (`--json` and friends), `HEY_NONINTERACTIVE`, a
non-TTY, and `--token`/`--cookie` logins all skip it, and `hey setup omarchy` is how a
script installs it — explicitly, in every output format, verified against the running
shell, with a loud failure when it cannot. A plugin you disable stays disabled until you
run `hey setup omarchy` again; `--remove` disables it and keeps the checkout.
The full `hey setup` wizard installs and enables the plugin automatically. Other interactive
sign-ins — `hey auth login` or any command's sign-in prompt — offer it once and remember
the answer. Those sign-in hooks never fire in scripts: machine output (`--json` and friends),
`HEY_NONINTERACTIVE`, a non-TTY, and `--token`/`--cookie` logins all skip it.
`hey setup omarchy` is the desktop-only path for scripts: it works in every output format,
verifies the running shell, and fails loudly when installation is incomplete. A plugin you
disable stays disabled until `hey setup` or `hey setup omarchy` enables it again; `--remove`
disables it and keeps the checkout.

Setup installs a `HEY TUI` launcher entry, a `HEY` row in the SUPER+SPACE menu, and a
`hey.toml.tpl` theme template so theme authors can tune the overlay. It prints the
Expand All @@ -701,22 +727,23 @@ other app. See [docs/omarchy.md](docs/omarchy.md) for the details and what is pl

hey-cli ships with an embedded agent skill so your coding agent can work with HEY on your
behalf, and a Claude Code plugin (`hey@37signals` from the `basecamp/claude-plugins`
marketplace). The setup wizard offers to connect detected agents; these commands do it on
their own:
marketplace). The setup wizard connects every detected agent automatically; these commands
manage the integrations on their own:

```bash
hey setup claude # install the skill and the hey@37signals plugin for Claude Code
hey setup codex # install the skill for Codex
hey skill install # install the skill only (~/.agents/skills/hey, linked for detected agents)
hey setup agents # non-interactive: skill + a single detected agent (the installer uses this)
hey doctor # check skill and plugin health per detected agent
hey setup agents # non-interactive: skill + a single detected agent (the installer uses this)
hey setup agents --remove # remove HEY's managed skills and Claude Code plugin
hey doctor # check skill and plugin health per detected agent
```

`hey setup agents` never prompts and never guesses: with several agents detected it installs
the skill only and lists the `hey setup <agent>` choices. `HEY_SETUP_AGENT=claude|codex|all|none`
picks explicitly. `HEY_NONINTERACTIVE=1` disables every prompt (the sign-in offer, the
wizard's confirmations) for harnesses that run hey under a pseudo-terminal. The installed
skill is refreshed automatically the first time a new hey release runs.
picks explicitly. `HEY_NONINTERACTIVE=1` disables interactive sign-in for harnesses that
run hey under a pseudo-terminal. The installed skill is refreshed automatically the first
time a new hey release runs.

hey only ever writes skill directories it owns: each one it creates carries a
`.managed-by-hey-cli` marker, and install, replacement and automatic refresh all refuse a
Expand Down
8 changes: 5 additions & 3 deletions docs/omarchy.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ step does not stop the others.
|---|---|---|
| Desktop entry | `~/.local/share/applications/HEY TUI.desktop` | Distinct from Omarchy's shipped `HEY.desktop` web app. Launches under app-id `org.omarchy.hey` |
| Menu row | marker block in `~/.config/omarchy/extensions/omarchy-menu.jsonc` | one root `HEY` row that focuses or launches the TUI; its guard is a PATH lookup, never network or `hey` itself. Becomes a submenu once there is more than one thing to open |
| Bar plugin | clone under `~/.config/omarchy/plugins/37signals.hey`, entry in `~/.config/omarchy/shell.json`'s bar layout | installed and enabled by **signing in with hey** (asked once — see below) or by this command, which also finishes an interrupted install and re-enables a plugin you disabled, verified against the running shell. `--notify` / `--no-notify` set or delete the entry's `notify` key, which the shell hot-reloads and the plugin reads to decide whether to toast. `--remove` disables the plugin and keeps its checkout. An earlier inline `hey-unread` module is removed on sight, its notify choice carried over |
| Bar plugin | clone under `~/.config/omarchy/plugins/37signals.hey`, entry in `~/.config/omarchy/shell.json`'s bar layout | installed and enabled automatically by the full **`hey setup`** wizard; other interactive sign-ins ask once (see below). This command also finishes an interrupted install and re-enables a plugin you disabled, verified against the running shell. `--notify` / `--no-notify` set or delete the entry's `notify` key, which the shell hot-reloads and the plugin reads to decide whether to toast. `--remove` disables the plugin and keeps its checkout. An earlier inline `hey-unread` module is removed on sight, its notify choice carried over |
| Theme template | `~/.config/omarchy/themed/hey.toml.tpl` | renders `hey.toml` into every theme so theme authors can override the overlay; triggers `omarchy-theme-refresh` |
| Keybinding | printed, never written | `o.bind("SUPER + SHIFT + ALT + H", "HEY TUI", "omarchy-launch-or-focus-tui --app-id=org.omarchy.hey hey tui")`; SUPER+SHIFT+E keeps opening the web app unless you `hl.unbind` it. Spelled out rather than `{ tui = "hey tui" }` because the lua helper quotes that into one word and the app-id derived from it would never match |

Expand All @@ -89,7 +89,7 @@ the current defaults, the layout goes too and the user is back to inheriting the
[`basecamp/omarchy-hey-plugin`](https://github.com/basecamp/omarchy-hey-plugin) is the
face: the HEY logo in the bar (tinted when there is unseen mail), the panel with its
account switcher, `New for you` / `Previously seen` tabs, the Screener count, mark-as-seen,
and the setup flow that installs `hey-cli` from the AUR and signs you in. hey-cli is the
and the setup flow that installs `hey-cli` through mise and signs you in. hey-cli is the
engine, and the plugin composes its generic commands: `hey box view imbox --json` for the Imbox,
`hey watch` to know when to read it again, `hey screener list --count` for the Screener,
`hey seen` for marking, `hey account list` and `hey auth status` for the rest. The
Expand Down Expand Up @@ -136,7 +136,9 @@ file, one lock:
- **Entry points.** An interactive OAuth sign-in — `hey auth login` / `hey login`,
`requireAuth`'s "Sign in now?" (how `hey tui` and every data command sign in), the lite
wizard — runs the routine in *ensure* mode and prints at most one stderr line; the full
wizard runs it as Step 3; `hey setup omarchy` runs it in *force* mode. The automatic
wizard runs its Omarchy step in *force* mode without asking (step numbers are
assigned dynamically to whichever steps actually run); `hey setup omarchy` also runs in
*force* mode. The automatic
hooks never run for machine output, `HEY_NONINTERACTIVE`, a non-TTY, or
`--token`/`--cookie` logins: a script installs with `hey setup omarchy`, which works in
every output format and exits `setup_failed` on any incomplete outcome — an incomplete
Expand Down
44 changes: 2 additions & 42 deletions internal/cmd/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,14 @@ package cmd
import (
"context"
"fmt"
"io"
"os"
"sort"
"strings"
"time"

"github.com/spf13/cobra"

"github.com/basecamp/hey-cli/internal/apierr"
"github.com/basecamp/hey-cli/internal/auth"
"github.com/basecamp/hey-cli/internal/harness"
"github.com/basecamp/hey-cli/internal/output"
)

Expand Down Expand Up @@ -97,7 +94,8 @@ Use --token or --cookie for non-interactive login.`,
if identity, err := rootSDK.Identity().GetIdentity(cmd.Context()); err == nil && identity != nil {
fmt.Fprintln(w, identityGreeting(identity))
}
printAgentNudge(w)
// Agent integration is configured through `hey setup`; sign-in reports
// the authenticated identity and completes its Omarchy integration.
ensureOmarchyBarPluginAfterLogin(cmd.ErrOrStderr())
return nil
}
Expand Down Expand Up @@ -313,41 +311,3 @@ func refuseSessionCookieAsToken() error {
}
return nil
}

// printAgentNudge prints a hint about coding agent setup after login.
//
// Detection proves presence, not intent: with a single detected-unhealthy
// agent it points at that agent; with several, it never guesses — it prints
// every `hey setup <id>` choice so the user picks. It never suggests
// `hey setup agents`, which is the installer's non-interactive path.
func printAgentNudge(w io.Writer) {
type nudgeAgent struct{ id, name string }
var unhealthy []nudgeAgent
for _, agent := range harness.DetectedAgents() {
if agent.Checks == nil {
continue
}
for _, c := range agent.Checks() {
if c.Status != "pass" {
unhealthy = append(unhealthy, nudgeAgent{id: agent.ID, name: agent.Name})
break
}
}
}
sort.Slice(unhealthy, func(i, j int) bool { return unhealthy[i].id < unhealthy[j].id })

switch len(unhealthy) {
case 0:
return
case 1:
fmt.Fprintln(w)
fmt.Fprintln(w, muted.format(fmt.Sprintf(" %s detected. Connect it to HEY:", unhealthy[0].name)))
fmt.Fprintln(w, bold.format(" hey setup "+unhealthy[0].id))
default:
fmt.Fprintln(w)
fmt.Fprintln(w, muted.format(" Multiple coding agents detected. Choose one:"))
for _, a := range unhealthy {
fmt.Fprintln(w, bold.format(" hey setup "+a.id))
}
}
}
38 changes: 0 additions & 38 deletions internal/cmd/auth_commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,41 +328,3 @@ func TestLoginLogoutShortcutsMirrorAuthCommands(t *testing.T) {
t.Errorf("hey login example = %q", login2.Example)
}
}

func TestPrintAgentNudge(t *testing.T) {
isolateAgents(t)
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
origColor := colorDisabled
colorDisabled = true
t.Cleanup(func() { colorDisabled = origColor })

var out bytes.Buffer
printAgentNudge(&out)
if out.Len() != 0 {
t.Errorf("no agents: nudge should be silent, got %q", out.String())
}

if err := os.MkdirAll(filepath.Join(home, ".claude"), 0o755); err != nil {
t.Fatal(err)
}
out.Reset()
printAgentNudge(&out)
if !strings.Contains(out.String(), "Claude Code detected. Connect it to HEY:") || !strings.Contains(out.String(), "hey setup claude") {
t.Errorf("single agent nudge = %q", out.String())
}
if strings.Contains(out.String(), "setup agents") {
t.Errorf("nudge must never suggest setup agents: %q", out.String())
}

if err := os.MkdirAll(filepath.Join(home, ".codex"), 0o755); err != nil {
t.Fatal(err)
}
out.Reset()
printAgentNudge(&out)
text := out.String()
if !strings.Contains(text, "Multiple coding agents detected. Choose one:") || !strings.Contains(text, "hey setup claude") || !strings.Contains(text, "hey setup codex") {
t.Errorf("multiple agent nudge = %q", text)
}
}
4 changes: 2 additions & 2 deletions internal/cmd/completion_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ func (c *completionInstallCommand) reportPackaged(cmd *cobra.Command, shell, pat
if writer.IsStyled() {
w := cmd.OutOrStdout()
for _, notice := range notices {
fmt.Fprintln(w, muted.format(notice))
fmt.Fprintln(w, notice)
}
}
return nil
Expand Down Expand Up @@ -559,7 +559,7 @@ func (c *completionInstallCommand) report(cmd *cobra.Command, env completionEnv,
if writer.IsStyled() {
w := cmd.OutOrStdout()
for _, notice := range notices {
fmt.Fprintln(w, muted.format(notice))
fmt.Fprintln(w, notice)
}
}
return nil
Expand Down
Loading
Loading