Skip to content

fix: confirm it's you before linking a Linear account - #357

Merged
kalaspuff merged 9 commits into
mainfrom
fctl/573-linear-links
Oct 30, 2026
Merged

kalaspuff merged 9 commits into
mainfrom
fctl/573-linear-links

Conversation

@fctl-sh

@fctl-sh fctl-sh Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

What a linked Linear account delegates to fctl, or mentions fctl in, starts sessions as its fctl person, on their Claude subscription, and its replies reach their sessions' agents. With a stolen browser session or token, someone could link their own Linear account to the victim's fctl account, with nothing to confirm and nothing said to the person, and go on starting the victim's sessions from Linear, reading what they do there, long after the stolen token was revoked or the password changed.

Linking a Linear account is now a sensitive action (Sensitive::LinkLinear), as connecting a Claude subscription became in #326: where the primary instance owner turned on confirming it's you, it takes a passkey or a code. The app's "Link your Linear account" buttons (Settings and a project's Tickets tab) confirm before they go on to Linear; a link opened elsewhere (Linear's own button in an agent session, fctl account linear connect) goes back to Settings with "Linking a Linear account takes confirming it's you", where linking again confirms first. A new link also emails the person when email is set up, naming the Linear account and saying what it does.

Verified in cargo xtask dev: with confirming turned on and an authenticator app on the account, an unconfirmed session's GET /api/v1/linear/link goes back to /settings?linear=confirm#linear, and after POST /api/v1/auth/verify it goes on to Linear's sign-in. The web tests for the Tickets tab and the outcomes pass, and cargo xtask check --quick passes. The email needs an instance with email, which the dev stack isn't.

The privacy policy lists the new email since:

Worth confirming

  • Decision: like every sensitive action, this asks only where confirming it's you is turned on, which is off by default; elsewhere the email is what tells the person.
  • Decision: a link from Linear's own button that needs confirming lands on Settings rather than taking up the agent session that asked for it; after linking from Settings, fctl still takes up the delegations that waited for that Linear account.

Stacked pull requests

The fixes before this one in this security pass are on main; the next ones build on this one:

fctl session


Note

Medium risk. It adds a confirm-it's-you check to linking a Linear account, which changes an authentication-adjacent flow for everyone who links one; a mistake could block linking or let a stolen session link an account.

What it does. Linking a Linear account lets Linear delegations start sessions as the person, so GET /api/v1/linear/link now requires the same sensitive-action confirmation as connecting a Claude subscription, where the instance asks for it. The web app confirms before sending people there, and Linear's own button is redirected back to Settings to confirm. A notice email is also sent when an account is linked.

New outcome and email. Linking now has a confirm outcome shown in Settings and sends a best-effort email to the person; docs/security.md and docs/architecture.md are updated.

fctl review's first look at 393e78c · 10 files, +173 −11 · Review agent settings

fctl-sh Bot added 5 commits October 30, 2026 22:34
A workspace's GitHub connection worked through whatever installation of
the GitHub App its account had, and a Linear one through fctl's app
wherever it was installed again: once uninstalled and installed again,
every connection made before came back. An account's or a Linear
workspace's admins had no way to end what they let a workspace reach,
such as a former admin's own workspace, which kept minting tokens there.

A GitHub connection now holds only through the installation it was made
through: sessions fail with a message that says so, and pull request news
stops, until the workspace's admins connect the account again with a
GitHub sign-in. When Linear says fctl's app was uninstalled, or refuses to
renew its token, the fctl workspaces' connections to that Linear workspace
end, and installing the app again connects only the fctl workspace it is
installed from.
Ending every fctl workspace's connection when Linear revokes fctl's app
deleted their rows, so a workspace's Linear tab showed nothing about why
its projects stopped working, and the hourly job then forgot the
installation. The connections stay now, revoked since
`workspace_linear.revoked_at` (a new migration), and the tab shows them
revoked with its prompt to connect again; installing the app again clears
only the connection of the workspace it is installed from, and nothing
takes tickets, delegations or replies through a revoked one.
…nt tokens

Connections to Linear workspaces whose installation was revoked before
this change had no `revoked_at`, so installing the app again would have
brought them all back: the migration marks them as of the revocation.

A renewal that Linear refused revoked the installation even when another
renewal had kept new tokens meanwhile, which now left every connection
revoked for good. Revoking after a refused renewal now takes that the
installation still has the tokens that were refused.
A renewal that kept new tokens after another renewal of the same token
had given up made the installation active again, but the connections that
giving up had revoked stayed revoked. The winner now clears the
revocation of the connections revoked with the installation, as it takes
the installation back.
What a linked Linear account delegates to fctl, or mentions it in,
starts sessions as its fctl person, on their Claude subscription, and its
replies reach their sessions' agents. With a stolen browser session or
token, someone could link their own Linear account to the victim's, with
nothing to confirm and nothing said, and go on starting the victim's
sessions from Linear long after the stolen token was revoked.

Linking a Linear account is now a sensitive action (`Sensitive::LinkLinear`),
which takes a passkey or a code where the instance asks for that: the app
confirms before it goes on to Linear, and a link opened elsewhere
(Linear's own button, the CLI) goes back to Settings to confirm and link
again. A new link also emails the person when email is set up, saying
which Linear account it is and what it does, as connecting a Claude
subscription does.
Bringing connections back when a renewal won a race also brought them
back when the uninstall webhook landed during a renewal, and a repeated
revocation moved the time they were matched by. A renewal now keeps its
new tokens only over the ones it renewed, and a revocation applies only
to an active installation: once revoked (uninstalled, or a renewal of its
current tokens refused), it stays so, with its connections, until an
admin connects it again. Linear answers a renewal it already answered the
same for half an hour, so two renewals of the same tokens don't refuse
each other.
kalaspuff pushed a commit to hyperstructure/fctl-site that referenced this pull request Oct 30, 2026
## Summary

app.fctl.sh now emails people when a Linear account is linked to their
account, as it does when a Claude subscription is connected, since what
that Linear account delegates to fctl then starts sessions as them. The
privacy policy's list of notice emails names it, and its updated date
moves to today.

The fctl change that sends it:

- hyperstructure/fctl#357

[fctl session](https://app.fctl.sh/s/ses_01M677K9YMM5WPZBTAR2KKXMSZ)

<!-- FCTL_REVIEW_SUMMARY -->
---

> [!NOTE]
> **Low risk.** It only edits the privacy policy text and its updated
date.
>
> **What it does.** Adds linking a Linear account to the list of account
changes that trigger a notice email in the privacy policy, and bumps the
policy's `updated` date to 2026-10-30.
>
> <sub>[fctl
review](https://app.fctl.sh/s/ses_01M67C25ZWPYSEQ7H51PE2CNMY)'s first
look at
[`d1fbde6`](d1fbde6)
· 1 file, +3 −3 · [Review agent
settings](https://app.fctl.sh/p/fctl?tab=reviews)</sub>

<!-- /FCTL_REVIEW_SUMMARY -->

Co-authored-by: fctl-sh[bot] <343971779+fctl-sh[bot]@users.noreply.github.com>
Base automatically changed from fctl/573-connections-end to main October 30, 2026 21:46
@fctl-review

This comment has been minimized.

Comment thread web/src/routes/settings.tsx Resolved
Comment thread web/src/lib/api.ts Resolved
Comment thread crates/fctl-domain/src/verify.rs Resolved
The links that link a Linear account confirmed it's you on every click,
so one meant to open a new tab (with Cmd, Ctrl or Shift) went in the
current one. `clickConfirmed` takes plain clicks alone and leaves the
others to the browser; the server sends such a tab back to confirm where
it must. Tests cover `goConfirmed`, `clickConfirmed`, the Tickets tab's
link and the action's wording.
@kalaspuff
kalaspuff added this pull request to stack #361 October 30, 2026 21:59
@kalaspuff
kalaspuff merged commit 725fc06 into main Oct 30, 2026
11 checks passed
@kalaspuff
kalaspuff deleted the fctl/573-linear-links branch October 30, 2026 22:14
kalaspuff pushed a commit that referenced this pull request Oct 30, 2026
## Summary

Session titles reached people's terminals as the server had them. `fctl
sessions`, `fctl show`, the header of `fctl attach` and `fctl run`, and
the host commands' lists printed them raw, and the titles the control
plane makes itself skipped the control-character check that titles
people set get: one made from a prompt's first line, from a Linear
ticket's title, a schedule's prompt or a reviewed pull request's title.
So a project editor, the author of a pull request a review agent
reviews, or whoever writes a Linear ticket that someone delegates could
put escape sequences in a title that, when another member listed
sessions, set their clipboard (in terminals that allow OSC 52), rewrote
their screen, set the window's title or planted links.

- Titles fctl makes leave out control characters and those that turn
text around (`fctl_domain::naming::plain`), as names already did.
- The CLI prints titles, descriptions, branches, names, models and what
`fctl login` shows (the instance's name, the code, the link, who signed
in) through `style::plain`, one line without any control characters, and
error messages through `terminal_safe`, as transcripts already were
(#174). Titles stored before this print clean too.

Verified against `cargo xtask dev`: a session started with a prompt that
begins with an OSC 52 sequence gets a title without it, and with a raw
escape sequence put into a stored title, `fctl sessions` and `fctl show`
print it without any escape or bell character. New unit tests cover the
domain's titles, review titles and the CLI's `plain`; `cargo xtask check
--quick` passes.

## Stacked pull requests

Based on the one before it in this security pass's stack:

- #357

[fctl session](https://app.fctl.sh/s/ses_01M677K9YMM5WPZBTAR2KKXMSZ)

<!-- FCTL_REVIEW_SUMMARY -->
---

> [!NOTE]
> **Low risk.** It only changes how titles and names are cleaned or
printed, in the CLI and in the titles the control plane derives, with no
stored-data or protocol change.
>
> **What it does.** Titles the control plane derives from prompts,
Linear tickets, schedules and reviewed pull requests now drop control
characters and characters that reverse text, so escape sequences in them
can no longer act on a terminal. The CLI also prints titles,
descriptions, branches, names, models, login details and error messages
through `style::plain` or `terminal_safe`, now including the `fctl run`
confirmation line and `fctl edit` output, so titles stored before this
print clean too.
>
> <sub>[fctl
review](https://app.fctl.sh/s/ses_01M67D4F4EMHJE3VPD7BTC633H)'s first
look at
[`10587ed`](10587ed)
· 8 files, +114 −25 · [Review agent
settings](https://app.fctl.sh/p/fctl?tab=reviews)</sub>

<!-- /FCTL_REVIEW_SUMMARY -->

---------

Co-authored-by: fctl-sh[bot] <343971779+fctl-sh[bot]@users.noreply.github.com>
Sign up for free to 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