Skip to content
Open
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
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,22 @@ Commands:
skills Manage WorkOS skills for coding agents (install, uninstall, list)
mcp Manage the WorkOS MCP server in coding agents
setup Set up WorkOS skills and the MCP server
```

**Nothing is installed into your coding agents without explicit opt-in.** The CLI never silently writes skills or MCP configuration into `~/.claude`, `~/.cursor`, etc. After `workos login` or `workos install`, an interactive session may offer to set up your agents — the prompt defaults to **No**, and declining (or running non-interactively) installs nothing. To opt in at any time:

```bash
workos setup # interactive setup (skills + MCP server)
workos setup --yes # non-interactive opt-in
workos skills install # skills only
workos mcp install # MCP server only
```

`workos setup` installs WorkOS skills and configures the MCP server only after consent. Use `workos skills list` to check skill status, `workos mcp status` to check whether the server definition is configured, or `workos doctor --fix` to refresh stale skills.
Use `workos skills list` to check skill status, `workos mcp status` to check whether the server definition is configured, or `workos doctor --fix` to refresh stale skills you previously installed.

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.

🔍 doctor --fix still installs skills into agents that never opted in

The new README text says workos doctor --fix merely refreshes "stale skills you previously installed", but maybeRefreshSkills in src/doctor/index.ts:50-56 triggers a refresh when any detected agent is stale OR has installedVersion === null (i.e. an agent directory exists but WorkOS skills were never installed), and refreshWorkOSSkills then writes skills to every detected agent. Under the PR's "nothing is installed without explicit opt-in" policy this path can still write into ~/.claude/~/.cursor for an agent the user never opted in for. Worth confirming whether --fix should be limited to agents with an existing marker.

Open in Devin Review

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


MCP configuration and OAuth authentication are separate states. The WorkOS CLI never inspects a coding agent's credentials, so "configured" means the server definition is in place — it cannot prove that OAuth is usable in any agent. Each agent owns its own OAuth; with Codex, for example, complete or refresh it with `codex mcp login workos` in your normal host shell. See the [WorkOS MCP setup and recovery guide](https://workos.com/docs/mcp) for user-global and trusted-project-only configuration.

```text
Resource Management:
organization (org) Manage organizations
user Manage users
Expand Down
33 changes: 33 additions & 0 deletions src/commands/setup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,15 @@ describe('runSetup — automatic triggers (login/install)', () => {
expect(prefs.recordSetupCompleted).not.toHaveBeenCalled();
});

it('defaults the consent prompt to No (AUTH-6734: install is explicit opt-in)', async () => {
detectSome();
vi.mocked(ui.confirm).mockResolvedValue(false);

await runSetup({ trigger: 'login' });

expect(ui.confirm).toHaveBeenCalledWith(expect.objectContaining({ initialValue: false }));
});

it('records an absolute decline and installs nothing on "no"', async () => {
detectSome();
vi.mocked(ui.confirm).mockResolvedValue(false);
Expand All @@ -214,6 +223,19 @@ describe('runSetup — automatic triggers (login/install)', () => {
expect(prefs.recordSetupCompleted).not.toHaveBeenCalled();
});

it('prints manual-install instructions when the user declines', async () => {
detectSome();
vi.mocked(ui.confirm).mockResolvedValue(false);

await runSetup({ trigger: 'login' });

const hints = vi.mocked(ui.log.hint).mock.calls.map(([msg]) => String(msg));
expect(hints.some((m) => m.includes('Nothing was installed'))).toBe(true);
expect(hints.some((m) => m.includes('workos setup'))).toBe(true);
expect(hints.some((m) => m.includes('workos skills install'))).toBe(true);
expect(hints.some((m) => m.includes('workos mcp install'))).toBe(true);
});

it('treats cancel (ctrl-c) as skip — no decline recorded, but emits a cancelled event', async () => {
detectSome();
vi.mocked(ui.confirm).mockResolvedValue(CANCEL);
Expand Down Expand Up @@ -307,6 +329,17 @@ describe('runSetup — command trigger', () => {
expect(prefs.recordSetupDeclined).not.toHaveBeenCalled();
});

it('scopes the decline instructions to what was offered', async () => {
vi.mocked(detectAgents).mockReturnValue([claudeAgent as any]);
vi.mocked(ui.confirm).mockResolvedValue(false);

await runSetup({ trigger: 'command', skillsOnly: true });

const hints = vi.mocked(ui.log.hint).mock.calls.map(([msg]) => String(msg));
expect(hints.some((m) => m.includes('workos skills install'))).toBe(true);
expect(hints.some((m) => m.includes('workos mcp install'))).toBe(false);
});

it('skillsOnly skips MCP detection/install', async () => {
vi.mocked(detectAgents).mockReturnValue([claudeAgent as any]);
vi.mocked(ui.confirm).mockResolvedValue(true);
Expand Down
29 changes: 27 additions & 2 deletions src/commands/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
* The consent contract is the whole point: nothing is written to a coding agent
* unless the user says yes (or passes --yes). This replaces the auto-install
* that a customer called "prompt injection malware".
*
* AUTH-6734 policy: never install silently. The consent prompt defaults to No,
* so an absent-minded Enter (or any non-answer) installs nothing; the only ways
* anything lands are an explicit "yes" at the prompt or an explicit flag
* (`workos setup --yes`, `workos skills install`, `workos mcp install`).
*/

import { homedir } from 'node:os';
Expand Down Expand Up @@ -144,7 +149,9 @@ export async function runSetup(opts: RunSetupOptions): Promise<void> {
`scaffold auth and manage WorkOS resources. Nothing is written until you confirm.`,
);

const answer = await ui.confirm({ message: 'Set up now?', initialValue: true });
// Default MUST stay No (AUTH-6734): installation is opt-in only, so the
// default answer — what an impatient Enter produces — installs nothing.
const answer = await ui.confirm({ message: 'Set up now?', initialValue: false });
// Cancel (ctrl-c) is not a decline — skip silently and ask again next time,
// but record it so the cut-off is observable in telemetry.
if (isCancel(answer)) {
Expand All @@ -154,7 +161,7 @@ export async function runSetup(opts: RunSetupOptions): Promise<void> {
if (!answer) {
if (!isCommand) recordSetupDeclined();
emitSetupEvent(opts.trigger, startedAt, 'declined', { skills: [], mcpInstalled: [], mcpFailed: [] });
ui.log.hint(`No problem. Run \`${formatWorkOSCommand('setup')}\` anytime.`);
printManualInstallInstructions(wantSkills, wantMcp);
return;
}
}
Expand Down Expand Up @@ -209,6 +216,24 @@ async function installAndReport(
reportResults(skillResult ? { agents: skillAgentNames, count: skillResult.skills.length } : null, mcpResults);
}

/**
* A decline is the safe default, not a dead end (AUTH-6734): always leave the
* exact manual-install commands behind so opting in later is self-serve.
* Scoped to what the offer actually covered (--skills-only / --mcp-only).
*/
function printManualInstallInstructions(wantSkills: boolean, wantMcp: boolean): void {
ui.log.hint('Nothing was installed. To install later, run any of:');
if (wantSkills && wantMcp) {
ui.log.hint(` ${formatWorkOSCommand('setup')} skills + MCP server`);
}
if (wantSkills) {
ui.log.hint(` ${formatWorkOSCommand('skills install')} skills only`);
}
if (wantMcp) {
ui.log.hint(` ${formatWorkOSCommand('mcp install')} MCP server only`);
}
}

interface SkillSummary {
agents: string[];
count: number;
Expand Down