Repository navigation
fix: confirm it's you before linking a Linear account - #357
Merged
Merged
Conversation
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>
This comment has been minimized.
This comment has been minimized.
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
approved these changes
Oct 30, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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'sGET /api/v1/linear/linkgoes back to/settings?linear=confirm#linear, and afterPOST /api/v1/auth/verifyit goes on to Linear's sign-in. The web tests for the Tickets tab and the outcomes pass, andcargo xtask check --quickpasses. The email needs an instance with email, which the dev stack isn't.The privacy policy lists the new email since:
Worth confirming
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/linknow 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
confirmoutcome 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