Skip to content

feat!: consent-gated agent setup and flat CLI output - #200

Merged
nicknisi merged 6 commits into
mainfrom
nicknisi/cli-ux
Jul 24, 2026
Merged

feat!: consent-gated agent setup and flat CLI output#200
nicknisi merged 6 commits into
mainfrom
nicknisi/cli-ux

Conversation

@nicknisi

@nicknisinicknisi commented Jul 22, 2026

Copy link
Copy Markdown
Member

Why

Two problems, one branch:

  1. Consent. A customer (SHPIT) called the CLI's skill auto-install "prompt injection malware": workos auth login and workos install silently and unconditionally installed all bundled skills to every detected coding agent, with no consent and no opt-out.
  2. Aesthetic. clack's gutter/box styling buried the important asks (install skills? connect MCP?) in cruft, and the post-login sequence felt spammy.

What changed

  • One consented setup moment. A new workos setup command owns skills + MCP together. Automatic offers after login/install are gated: silent in agent/CI/non-TTY/JSON, never re-ask after a decline or completion, and nothing is written to a coding agent until the user confirms (or passes --yes). This replaces the silent auto-install.
  • Flat, gutterless output. Swapped the clack engine for @inquirer/prompts behind the existing single UI facade (src/utils/ui.ts). Output is hand-styled: branded intro (WorkOS · AuthKit installer), aligned key/value rows, nested sub-steps, minimal glyphs, one accent color.
  • De-boxed notices. The telemetry notice is now a flat line; the unclaimed-environment warning uses a WARN pill (new renderStderrNotice).
  • Security fix.allowedTools: [] so installer tool calls route through canUseTool — this re-arms the Bash safety gate that bare tool names were bypassing, and silences the SDK's CLAUDE_SDK_CAN_USE_TOOL_SHADOWED warning.

BREAKING CHANGE

workos auth login and workos install no longer auto-install skills. Skills + MCP are installed only through the consented setup flow (workos setup, or the post-login/install offer on a human TTY). Agent / CI / non-TTY / JSON runs write nothing.

Test plan

  • pnpm build
  • pnpm typecheck
  • pnpm lint
  • pnpm test (2175 tests pass)
  • New commands include JSON mode support and tests

Screenshots

capture_20260722_111428capture_20260722_122854capture_20260722_123045capture_20260722_123149

@greptile-apps

greptile-appsBot commented Jul 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds consent-based coding-agent setup and simplifies CLI output. The main changes are:

  • A new workos setup command for skills and MCP configuration.
  • Consent gates for automatic setup after login and install.
  • Inquirer-based prompts behind the existing UI facade.
  • Flat notices, summaries, and command output.
  • Restored Bash tool checks for installer agent calls.

Confidence Score: 5/5

The latest changes appear safe to merge.

  • No additional blocking issue remains that requires a separate fix.

Important Files Changed

FilenameOverview
src/commands/setup.tsAdds the consented setup flow, automatic-offer gates, installation reporting, and persisted setup state.
src/utils/ui.tsReplaces the prompt implementation with Inquirer while preserving the shared UI facade.
src/lib/preferences.tsAdds persisted setup decline and completion preferences.

Reviews (6): Last reviewed commit: "refactor: dedup UI palette, lazy-load in..." | Re-trigger Greptile

Comment threadsrc/commands/setup.ts Outdated

@devin-ai-integrationdevin-ai-integrationBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Open in Devin Review

Comment threadsrc/commands/setup.ts Outdated
Comment on lines +114 to +120
} else if (!isPromptAllowed() && !opts.assumeYes) {
// Explicit `workos setup` in a non-interactive context needs --yes.
exitWithError({
code: 'confirmation_required',
message: `Interactive setup needs a TTY. Re-run \`${formatWorkOSCommand('setup --yes')}\` to install non-interactively.`,
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 workos setup --json on a TTY prompts interactively and corrupts JSON stdout

In the command trigger path, gating only blocks non-interactive contexts: else if (!isPromptAllowed() && !opts.assumeYes) errors, but there is no isJsonMode() guard (src/commands/setup.ts:114-120). If a user runs workos setup --json on an interactive TTY, output mode is JSON while interaction mode stays human, so isPromptAllowed() is true and assumeYes is false. The flow then falls through to the offer block (src/commands/setup.ts:143-160) and calls ui.heading/ui.note (which console.log human text) and ui.confirm (an interactive prompt), polluting the machine-readable stdout stream. The automatic (login/install) path explicitly guards isJsonMode() (src/commands/setup.ts:112) and the api command errors in JSON mode rather than prompting, so this is an inconsistency. Note that connection/directory delete share the same TTY-prompt-without-JSON-guard pattern, so this may be an accepted repo convention; worth confirming whether workos setup --json on a TTY should instead require --yes.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment threadsrc/commands/setup.ts
Comment on lines +176 to +195
const [skillResult, mcpResults] = await Promise.all([
skillAgents.length > 0 ? refreshWorkOSSkills({ agents: skillAgents }) : Promise.resolve(null),
Promise.all(mcpTargets.map((target) => target.add())),
]);
const skillAgentNames = skillResult?.agents.map((a) => a.displayName) ?? [];

recordSetupCompleted();

const mcpInstalled = mcpResults
.filter((r) => r.outcome === 'installed' || r.outcome === 'already-installed')
.map((r) => r.agent);
const mcpFailed = mcpResults.filter((r) => r.outcome === 'failed').map((r) => r.agent);

emitSetupEvent(opts.trigger, startedAt, true, {
skills: skillResult?.agents.map((a) => a.name) ?? [],
mcpInstalled,
mcpFailed,
});

reportResults(skillResult ? { agents: skillAgentNames, count: skillResult.skills.length } : null, mcpResults);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 recordSetupCompleted() fires even when every install fails

installAndReport calls recordSetupCompleted() (src/commands/setup.ts:182) unconditionally after the install attempt, before checking outcomes. If the user consented but every MCP add() returned failed (and/or refreshWorkOSSkills returned null), completion is still persisted. Because isSetupCompleted() suppresses all future automatic offers (src/commands/setup.ts:113), a user whose setup failed transiently during a post-login/post-install offer will never be auto-offered again; they'd have to run workos setup manually. The old standalone MCP offer only recorded a decline on an explicit "no", never marking completion on failure. This is a behavioral trade-off (treat consent as completion) rather than a clear defect, but may not be the intended UX for failed installs.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Two problems, one change:
1. Consent — a customer called the CLI's skill auto-install "prompt injection
malware": `workos auth login` / `workos install` silently and unconditionally
installed all bundled skills to every detected coding agent.
2. Aesthetic — clack's gutter/box styling buried the important asks and the
post-login sequence felt spammy.
What changed:
- One consented setup moment: a new `workos setup` owns skills + MCP together.
Automatic offers after login/install are gated (silent in agent/CI/non-TTY/
JSON, never re-ask after a decline/completion) and nothing is written until
the user confirms (or passes --yes). Replaces the silent auto-install.
- Flat, gutterless output: swapped the clack engine for @inquirer/prompts behind
the single UI facade (src/utils/ui.ts). Hand-styled: branded intro, aligned
key/value rows, nested sub-steps, one accent color. De-boxed notices
(telemetry = flat line, unclaimed-env = WARN pill).
- Security fix: installer tool calls route through canUseTool (allowedTools: [])
so installerCanUseTool's Bash allowlist actually runs — bare tool names had
been auto-approving Bash and bypassing the gate (also silenced the SDK
CLAUDE_SDK_CAN_USE_TOOL_SHADOWED warning).
BREAKING CHANGE: `workos auth login` and `workos install` no longer auto-install
skills. Skills + MCP are installed only through the consented setup flow
(`workos setup`, or the post-login/install offer on a human TTY). Agent / CI /
non-TTY / JSON runs write nothing.
…etry
Follow-up UX + reliability pass on the CLI overhaul (PR #200).
Prompts
- Serialize all prompts through a single-flight mutex in the ui facade and
pause any active spinner around a prompt. Fixes the concurrent git-dirty +
protected-branch prompts that opened on one stdin (the "asks a question but
moves on" bug), and stops a spinner's redraw from clobbering a prompt.
- Guard prompts against JSON / non-TTY contexts; route JSON output to the
headless adapter and have the CLI adapter fail fast on an unanswerable prompt
instead of hanging or silently crashing.
- Abort queued sibling prompts when a run is cancelled, so a cancelled run can't
leave a now-moot question open.
- Remove the 30s deadline that auto-dismissed the "Set up now?" confirm.
Output
- Replace the block-letter banner with a compact lock brand mark, flatten the
completion summary and the provisioned/unclaimed stderr notices, and make the
unclaimed-environment warning bolder with a clear claim CTA.
Errors + telemetry
- Rewrite install errors for humans with recovery hints and word-boundary
matching (no more "author" reading as "auth").
- Emit telemetry for login staging/refresh/auth failures, setup cancel/errors,
and MCP install failure reasons.
- Fix abortIfCancelled flushing a "cancelled" session on every prompt (bogus
session end plus ~3s of dead-time per prompt).
Address Devin review findings on the consent-gated setup flow.
- `workos setup --json` on a TTY no longer prompts. Output mode is JSON
while interaction mode stays human, so isPromptAllowed() is true and the
command path fell through to ui.confirm/ui.note, corrupting the
machine-readable stdout stream. Require --yes under --json (mirrors the
api command), consistent with the automatic path's isJsonMode() guard.
- recordSetupCompleted() now fires only when something actually installed.
A run where every install failed (e.g. transient `claude mcp add`
timeouts, no skills to fall back on) previously persisted completion,
and isSetupCompleted() suppresses all future automatic offers -- so a
transient failure permanently stranded the user. Leave the pref unset on
total failure so the next login/install re-offers.
… lifecycle
Quality cleanups from a /simplify pass on the CLI UX refactor:
- Move the WorkOS brand palette to a single `palette` in cli-symbols.ts;
ui.ts and summary-box.ts now share it instead of redefining the hexes.
- Lazy-load the @inquirer/prompts barrel inside the four prompt fns so its
ten widgets stay off every non-interactive path (--json, --version,
resource commands) — most invocations. import() is module-cached.
- Route stray blank-line writes through a guarded blank() sink and drop the
redundant per-function dashboard-mode guards in intro/outro/heading/note.
- Extract withPromptActive() in the CLI adapter, collapsing the copy-pasted
isPromptActive/flushPendingLogs dance across six prompt handlers into one
exception-safe try/finally.
- Remove dead log.message and the never-passed note() title param.
@nicknisi
nicknisi merged commit 9b0deb8 into mainJul 24, 2026
6 checks passed
@nicknisi
nicknisi deleted the nicknisi/cli-ux branch July 24, 2026 15:44
@github-actionsgithub-actionsBot mentioned this pull request Jul 23, 2026
gjtorikian added a commit that referenced this pull request Jul 26, 2026
Resolves conflicts from the #195 squash-merge (Bun standalone) and the
#200 consent-gated setup / flat output rework:
- release.yml: keep the publish-homebrew job on top of main's release flow
- install.ts / bin.ts: take main's maybeRunSetupAfter flow; drop the
superseded autoInstallSkills + maybeOfferMcpInstall wiring
- clack -> ui module rename applied across integrations and specs
- version-check.ts: keep install-aware upgradeNotice() over the static URL
- install.spec.ts: drop a duplicated describe block referencing clack
- bun.lock: regenerated against the merged package.json
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@nicknisi