Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions internal/cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,28 @@ func TestRequireAuthPromptsOnlyWhenInteractiveAndStyled(t *testing.T) {
t.Errorf("prompt shown %d times for machine output", *asked)
}
})

t.Run("never prompts when non-interactive is requested", func(t *testing.T) {
t.Setenv("HEY_NONINTERACTIVE", "1")
previousStdin, previousStdout, previousStderr := stdinIsTerminal, stdoutIsTerminal, stderrIsTerminal
stdinIsTerminal = func() bool { return true }
stdoutIsTerminal = func() bool { return true }
stderrIsTerminal = func() bool { return true }
t.Cleanup(func() {
stdinIsTerminal, stdoutIsTerminal, stderrIsTerminal = previousStdin, previousStdout, previousStderr
})
asked := stubAskToSignIn(t, true)
prev := writer
writer = output.New(output.Options{Format: output.FormatStyled, Stdout: &bytes.Buffer{}, Stderr: &bytes.Buffer{}})
t.Cleanup(func() { writer = prev })

if err := requireAuth(); err == nil {
t.Fatal("expected an auth error")
}
if *asked != 0 {
t.Errorf("prompt shown %d times with HEY_NONINTERACTIVE", *asked)
}
})
}

func TestDataCommandWithoutAuthReturnsAuthErrorWhenPiped(t *testing.T) {
Expand Down
35 changes: 35 additions & 0 deletions skills/embed_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package skills

import (
"strings"
"testing"
)

func TestHeySkillReusesAuthenticationForUnattendedAgents(t *testing.T) {
data, err := FS.ReadFile("hey/SKILL.md")
if err != nil {
t.Fatal(err)
}
content := string(data)

for _, want := range []string{
"hey auth status --json",
"when an explicit authentication check is needed",
"HEY_NONINTERACTIVE=1",
"Never run `hey auth login` unattended",
"report the task as blocked",
} {
if !strings.Contains(content, want) {
t.Errorf("embedded HEY skill does not contain %q", want)
}
}

for _, forbidden := range []string{
"run `hey auth login` first",
"before the first data command",
} {
if strings.Contains(content, forbidden) {
t.Errorf("embedded HEY skill still requires an authentication preflight with %q", forbidden)
}
}
}
39 changes: 23 additions & 16 deletions skills/hey/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ CLI for HEY: mailboxes, labels, collections, email threads, contacts, replies, c
**MUST follow these rules:**

1. **Choose the right structured output** — use `--jq '<expression>'` to filter or extract fields and `--json` for the full response. Never pipe to an external `jq`; `--jq` is built in and implies `--json`.
2. **Authentication required** for all data commands — run `hey auth login` first
2. **Reuse stored authentication** — run the requested data command; it uses stored credentials and refreshes expiring OAuth tokens automatically. If it returns an auth error, report the task as blocked. Use `hey auth status --json` when an explicit authentication check is needed. Never run `hey auth login` unattended; use it only for interactive recovery with the user present.
3. **HTML output** is available via `--html` for commands that return HTML content
4. **Linked mail accounts share one login** — use `hey account list --json`, then `--account <id|all>` when a task must target one account
5. **Local HEY configuration requires human trust** — never run `hey config trust-local` without the user's explicit approval
Expand Down Expand Up @@ -246,7 +246,7 @@ notice on stderr. Both need list data, so they work on `hey box list`, `hey box
| List journal entries | `hey journal list --json` |
| Read journal entry | `hey journal read 2024-03-15 --json` |
| Write journal entry | `hey journal write "Shipped the pagination fix."` (empty content removes the entry) |
| Check auth status | `hey auth status` |
| Check auth status | `hey auth status --json` |
| Print bearer token | `hey auth token` (refuses a `--cookie` login) |
| Launch TUI | `hey tui` (Ctrl+A switches linked mail accounts) |

Expand Down Expand Up @@ -751,20 +751,27 @@ error.

### Authentication

Data commands use the credentials HEY already stores and refresh expiring OAuth tokens automatically. Run the requested data command without a login preflight. Use `hey auth status --json` when the user asks for authentication status or when an explicit authentication check helps diagnose a failure; it reports whether credentials are available without changing them.

If a data command returns exit code 3 with `"code": "auth"`, report that authentication is required and the task is blocked. Tell the user to run `hey auth login`; do not run it for them unattended.

Piped, machine-output and non-TTY commands do not prompt for sign-in. When an agent harness runs commands under a PTY, set `HEY_NONINTERACTIVE=1` so a missing login returns the same actionable auth error instead of opening an interactive prompt.

```bash
hey auth login # Log in (browser-based OAuth)
hey auth status # Check if authenticated
hey auth logout # Log out
hey login / hey logout # Shortcuts for the two above
hey setup omarchy # Omarchy only: put HEY in the bar. The interactive
# sign-in offer never fires for agents (non-TTY,
# machine output), so this command is the way
hey setup # First-run wizard: sign in + connect coding agents
HEY_NONINTERACTIVE=1 hey setup --json # No prompts and no OAuth wait — but still
# installs agent skills and records onboarding;
# use `hey doctor` to inspect without changes.
# (Without HEY_NONINTERACTIVE, a terminal on
# stdin still starts browser sign-in.)
hey auth status --json # Inspect stored authentication without changing it
HEY_NONINTERACTIVE=1 hey box list --json # A PTY-safe unattended data command
hey auth login # Interactive browser recovery, with the user present
hey auth logout # Log out
hey login / hey logout # Shortcuts for the two above
hey setup omarchy # Omarchy only: put HEY in the bar. The interactive
# sign-in offer never fires for agents (non-TTY,
# machine output), so this command is the way
hey setup # First-run wizard: sign in + connect coding agents
HEY_NONINTERACTIVE=1 hey setup --json # No prompts and no OAuth wait — but still
# installs agent skills and records onboarding;
# use `hey doctor` to inspect without changes.
# (Without HEY_NONINTERACTIVE, a terminal on
# stdin still starts browser sign-in.)
```

If a command fails with an auth error, run `hey auth status` to check, then `hey auth login` to re-authenticate.
Run `hey auth login` only when the user is present and explicitly asks to authenticate.
Loading