Linear Hardening Guide
Issue tracking platform hardening for Linear including SAML SSO and SCIM, login-method restriction, workspace access, team permissions, and integration security
Overview
Linear is a modern issue tracking and project management platform designed for software teams. As a platform managing development workflows and project data, Linear security configurations directly impact operational security and intellectual property protection.
Intended Audience
- Security engineers managing engineering tools
- IT administrators configuring Linear
- Engineering managers managing workspaces
- GRC professionals assessing development security
How to Use This Guide
- L1 (Crawl): Essential controls for all organizations
- L2 (Walk): Enhanced controls for security-sensitive environments
- L3 (Run): Strictest controls for regulated industries
Scope
This guide covers Linear security including SAML SSO and SCIM provisioning, login-method restriction, workspace membership and invitations, roles and team permissions, integration and API-key security, audit logging, and data residency.
Automation surfaces. Linear’s administrative automation surface is its GraphQL API at https://api.linear.app/graphql; the official TypeScript SDK (@linear/sdk) wraps the same schema. Linear publishes no CLI and no Terraform provider of its own. A community provider (terraform-community-providers/linear) exists, but its linear_workspace_settings resource resets every setting you leave out to the provider’s default, so this guide does not use it. The API packs below are read-only audits. Settings that decide who can sign in are left to the console on purpose, because a wrong value locks people out.
Table of Contents
- Authentication & SSO
- Access Controls
- Integration Security
- Monitoring & Compliance
- Compliance Quick Reference
1. Authentication & SSO
1.1 Configure SAML Single Sign-On
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 6.3, 12.5 |
| NIST 800-53 | IA-2, IA-8 |
Description
Configure SAML SSO so Linear sign-in runs through your identity provider. Claim your email domains so SAML is required for them, and enable SCIM so the IdP creates and suspends Linear accounts.
Rationale
Why This Matters:
- Centralizing Linear authentication in your corporate IdP enforces MFA, conditional access, and session policies on every login
- Without SAML, members sign in with a Google account, an emailed login link or code, or a passkey, all outside your IdP’s policies. Linear has no passwords, so the emailed link is the phishable path
- Once SAML is enabled, members on SAML-approved (DNS-claimed) domains must sign in through SAML by default
- SAML’s just-in-time provisioning creates accounts but never removes them. SCIM suspends a Linear account when the IdP deactivates the user, which closes orphaned access to roadmaps and issues
- Linear workspaces hold product roadmaps, security issues, and internal planning, so a single compromised login can expose sensitive engineering intelligence
Attack Prevented: Credential theft, phishing, account takeover, orphaned-account access
Prerequisites
- Enterprise plan (SAML and SCIM are Enterprise features)
- Linear workspace admin or owner access
- A SAML 2.0 identity provider. Linear documents Okta, Microsoft Entra ID, Google Workspace, and OneLogin, plus a custom SAML 2.0 integration
- DNS access to publish a TXT record for each domain you claim
ClickOps Implementation
Step 1: Open SAML & SCIM Settings
- Navigate to: Settings → Administration → Security
- Under Authentication methods, click Configure next to SAML & SCIM
Step 2: Exchange Metadata with Your IdP
- Create a SAML 2.0 application in your IdP and enter the details Linear shows you. In Okta, Linear’s ACS URL (it ends in
/acs) is the Single sign-on URL - Press Continue, then give Linear your IdP’s metadata, either as a metadata XML URL or as the raw XML. Metadata flows from the IdP to Linear: Okta’s Metadata URL, Entra ID’s Federation Metadata XML, or Google Workspace’s downloaded metadata
- In Entra ID, set the NameID to Email Address format. Linear takes each user’s email from the SAML NameID
- To change the configuration later, use … → Edit Configuration on the same settings page
Step 3: Claim Your Domains so SAML Is Required
- Add each corporate domain under the SAML-approved email domains setting, and publish the DNS TXT record Linear gives you to claim it
- Once SAML is enabled, members on SAML-approved domains must sign in through SAML by default. Existing sessions are not logged out, but those users must use SAML at their next sign-in
- Allow non-SAML logins only for specific other email domains, such as contractors without IdP accounts, and keep that list short
- Use the option to prevent non-admins from creating new Linear workspaces with a claimed-domain email (Linear’s docs call it Disable new workspace creation)
- Before you rely on enforcement, complete a SAML sign-in yourself. Owners and admins may be able to sign in with other methods to prevent lockouts, depending on your authentication settings. That is a safety net, not a test
Step 4: Enable SCIM Provisioning
- On the same SAML & SCIM page, turn on SCIM and click View configuration to get the SCIM base connector URL and bearer token. Treat the bearer token as a secret
- Enter both values in your IdP’s provisioning settings. Linear has tested Okta and OneLogin
- SCIM provisions users as Members by default. To assign roles from the IdP, push
linear-owners(Enterprise),linear-admins, andlinear-guestsgroups - After SCIM is enabled, manage members and admins in the IdP. A user deactivated there is suspended in Linear
Time to Complete: ~1-2 hours
Code Implementation
Linear’s public GraphQL API has no write interface for SAML or SCIM: OrganizationUpdateInput carries no SAML or SCIM field, so configuration is console-only. The pack below is the read side. It proves both are enabled.
Code Pack: API Script
Q_SAML_SCIM='query HthLinearSamlScim { organization { samlEnabled scimEnabled } }'
audit_saml_scim() {
gql "${Q_SAML_SCIM}"
local saml scim
saml="$(dq -r '.organization.samlEnabled')"
scim="$(dq -r '.organization.scimEnabled')"
echo "Linear 1.1: SAML SSO and SCIM provisioning"
echo " samlEnabled: ${saml}"
echo " scimEnabled: ${scim}"
case "${saml}" in
true) ;;
false) finding "SAML is not enabled. Members sign in with Google, an emailed login link or code, or a passkey, outside your IdP's MFA and session policy (SAML requires the Enterprise plan)." ;;
*) undetermined "samlEnabled came back as '${saml}', not a boolean." ;;
esac
case "${scim}" in
true) ;;
false) finding "SCIM is not enabled. SAML just-in-time provisioning creates accounts but never suspends them, so offboarding stays a manual step." ;;
*) undetermined "scimEnabled came back as '${scim}', not a boolean." ;;
esac
}
1.2 Restrict Login Methods to SAML or Passkeys
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 6.5 |
| NIST 800-53 | IA-2(1) |
Description
Use Linear’s Restrict login methods setting to require specific sign-in methods for every member: SAML where you have it, passkeys otherwise. Without it, Google, emailed login link, passkey, and SAML all stay open. Linear has no two-factor authentication setting of its own; the lever is which methods you allow.
Rationale
Why This Matters:
- Linear has no passwords and no built-in second factor. Members sign in with a Google account, an emailed login link or code, a passkey, or SAML, so a Linear login is only as strong as the weakest method you allow
- With no restriction, every method stays open to every member, and an attacker needs only the weakest one, for example a phished email login code
- Requiring SAML moves MFA, device posture, and session policy into your IdP. Requiring passkeys gives an origin-bound credential that a phishing proxy cannot relay
- Owners and admins can still sign in with any method, so they can’t be locked out of this setting. That makes those few accounts the ones to protect most (see 2.3)
- Linear issues and projects can reveal unreleased features and security work that attackers actively seek
Attack Prevented: Phishing of emailed login links and codes, account takeover through a weaker sign-in method, adversary-in-the-middle relay of non-phishing-resistant logins
Linear has no two-factor authentication setting. Earlier versions of this control told you to turn on a workspace Require two-factor authentication toggle. Linear’s login-methods documentation lists four sign-in methods (Google, email, passkey, SAML) and no second factor, and the enforcement lever is Restrict login methods (Login methods, checked 2026-09-24).
Prerequisites
- Business or Enterprise plan (login restrictions); Enterprise plan for SAML
- Linear workspace admin or owner access
ClickOps Implementation
Step 1: Register Passkeys First
- Each member adds a passkey from Settings → Account → Security & Access. Several devices can be registered
- Passkeys aren’t supported in the Linear desktop app. Before you require them, confirm your members can sign in through a browser or the mobile app
- Start with owners and admins, and with members who can see private teams
Step 2: Restrict Login Methods
- Navigate to: Settings → Administration → Security
- Use Restrict login methods to allow only SAML (once 1.1 is complete) or passkey
- Before saving, confirm that at least one owner or admin has completed a sign-in with the method you are requiring
Step 3: Enforce MFA in the IdP
- When SAML is the required method, require MFA in your IdP for the Linear application, and phishing-resistant methods (FIDO2/WebAuthn) for admins
- Login-method restrictions do not apply to guests invited directly to the workspace. Provision guests through your IdP when they need the same bar
Code Implementation
The login-method list can be set through the API (organizationUpdate with authSettings). This guide deliberately ships only the read side, because a mistaken restriction locks members out.
Code Pack: API Script
Q_LOGIN_METHODS='query HthLinearLoginMethods { organization { authSettings allowedAuthServices } }'
APPROVED_AUTH_SERVICES="${LINEAR_APPROVED_AUTH_SERVICES:-saml}"
audit_login_methods() {
gql "${Q_LOGIN_METHODS}"
local source allowed outside bypass
# authSettings.allowedAuthServices is current; the top-level field is its deprecated mirror.
if dq -e '.organization.authSettings | type == "object" and has("allowedAuthServices")' >/dev/null; then
allowed="$(dq -c '.organization.authSettings.allowedAuthServices')"
source="authSettings.allowedAuthServices"
else
allowed="$(dq -c '.organization.allowedAuthServices')"
source="allowedAuthServices (deprecated mirror)"
fi
echo "Linear 1.2: login-method restriction"
if ! printf '%s' "${allowed}" | jq -e 'type == "array" and all(.[]; type == "string")' >/dev/null; then
undetermined "${source} is not a list of strings: ${allowed}"
return 0
fi
echo " allowed login methods (${source}): $(printf '%s' "${allowed}" | jq -r 'if length == 0 then "ALL (no restriction)" else join(", ") end')"
echo " approved set (LINEAR_APPROVED_AUTH_SERVICES): ${APPROVED_AUTH_SERVICES}"
if [ "$(printf '%s' "${allowed}" | jq 'length')" -eq 0 ]; then
finding "no login-method restriction. Every sign-in method (Google, emailed login link or code, passkey, SAML) is open to every member (Business/Enterprise plans can restrict it)."
else
# TRAP 2: compare case-insensitively against the operator's approved set.
outside="$(printf '%s' "${allowed}" | jq -r --arg ok "${APPROVED_AUTH_SERVICES}" '
($ok | split(",") | map(ascii_downcase | gsub("^\\s+|\\s+$"; "")) | map(select(. != ""))) as $approved
| [ .[] | select((ascii_downcase) as $s | ($approved | index($s)) == null) ] | join(", ")')"
if [ -n "${outside}" ]; then
finding "login methods outside the approved set are allowed: ${outside}"
fi
fi
# TRAP 3: informational only.
# `//` would turn a real `false` into the fallback, so test for the key instead.
bypass="$(dq -r '(.organization.authSettings // {}) | if has("disableAuthServiceBypass") then (.disableAuthServiceBypass | tostring) else "unset" end')"
echo " owner/admin any-method bypass disabled: ${bypass} (informational; the bypass is Linear's lockout safeguard)"
}
Source: Login methods, Security & Access, SAML
1.3 Configure Allowed Domains
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 5.3 |
| NIST 800-53 | AC-2 |
Description
Decide who can join the workspace without an explicit invitation, and who may send invitations. Keep approved email domains to domains your organization owns, limit invitations to admins, and keep the reusable invite link off.
Rationale
Why This Matters:
- An approved email domain is an auto-join rule, not a block: anyone with a matching address can join the workspace without an invitation or approval, so every listed domain is a standing grant of access
- Linear states that the setting does not prevent users from creating new workspaces with that domain’s email. Blocking separate workspaces is the job of the SAML domain claim in 1.1
- A domain you let lapse or transfer keeps auto-join open to whoever controls it next, which is why Linear tells admins to remove such domains
- On paid plans only admins can invite by default. Turning on Allow users to send invites, or sharing the persistent invite link, widens who can bring people in
Attack Prevented: Unauthorized workspace joining, access through lapsed or transferred domains, invite-link leakage
This control was reframed in 2026-09. Earlier versions described approved domains as a way to block public email providers and outsiders. Linear’s documentation describes an auto-join convenience that “does not prevent users from creating new workspaces with that domain email”, so the hardening is to keep the list minimal (Invite members, checked 2026-09-24).
ClickOps Implementation
Step 1: Review Approved Email Domains
- Navigate to: Settings → Administration → Security
- Keep the approved email domains to domains your organization owns and controls. Never list a public email provider’s domain, because every address on it could join
- Remove any domain you have cancelled or transferred to another organization
- Re-review the list on a regular schedule
Step 2: Restrict Invitations
- On the same page, leave Allow users to send invites off so only admins can invite members (paid plans; on the Free plan every member is an admin)
- Leave the invite link disabled. If you must use one, share it internally only and click Reset invite link whenever it may have leaked. Invite links are unavailable in SAML- and SCIM-enabled workspaces
Step 3: Don’t Confuse the Two Domain Settings
- The approved email domains here only streamline joining
- The DNS-claimed SAML-approved domains in 1.1 are what make SAML sign-in mandatory for a domain
Code Implementation
Linear’s public GraphQL schema has mutations for domains but no query that lists a workspace’s approved email domains, so reviewing that list (Step 1) is console-only. The pack audits who may send invitations.
Code Pack: API Script
Q_INVITATIONS='query HthLinearInvitations { organization { securitySettings allowMembersToInvite } }'
audit_invitations() {
gql "${Q_INVITATIONS}"
local role legacy
role="$(dq -r '(.organization.securitySettings // {}).invitationsRole // ""')"
legacy="$(dq -r '.organization.allowMembersToInvite | if . == null then "" else tostring end')"
echo "Linear 1.3: who may invite people into the workspace"
if [ -n "${role}" ]; then
echo " securitySettings.invitationsRole: ${role}"
case "${role}" in
owner|admin) ;;
user|guest) finding "invitationsRole is '${role}': members below admin can invite people into the workspace (\"Allow users to send invites\" is on)." ;;
*) undetermined "invitationsRole has an unrecognised value '${role}'." ;;
esac
elif [ -n "${legacy}" ]; then
echo " allowMembersToInvite (deprecated mirror): ${legacy}"
case "${legacy}" in
false) ;;
true) finding "\"Allow users to send invites\" is on: any member can invite people into the workspace." ;;
*) undetermined "allowMembersToInvite has an unrecognised value '${legacy}'." ;;
esac
else
# TRAP 2
undetermined "neither securitySettings.invitationsRole nor allowMembersToInvite is set, so the effective invitation policy could not be read."
fi
# TRAP 1
echo " approved email domains: NOT CHECKED. No public query lists them; review Settings > Administration > Security."
}
Source: Invite members, SAML
2. Access Controls
2.1 Configure Team Permissions
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 5.4 |
| NIST 800-53 | AC-6 |
Description
Implement least privilege with Linear’s roles and teams. Give each person the lowest workspace role that fits, restrict team creation to admins, and use team owners to delegate team administration.
Rationale
Why This Matters:
- Scoping members to only the teams they need limits how much of the workspace any single compromised account can reach
- Linear’s roles (Workspace owner, Admin, Team owner, Member, Guest) separate what ordinary users can change from what administrators can, and Guests see only the teams they are added to
- On the Free plan every user is an Admin, so least privilege in Linear is only possible on a paid plan
- Function-based teams contain the blast radius of a phished or insider account to a narrow slice of issues
- Regular access reviews catch privilege creep before it becomes a standing risk
Attack Prevented: Privilege escalation, lateral movement, insider data access, over-permissioned accounts
Prerequisites
- Linear workspace admin access
- Business or Enterprise plan for the Team owner and Guest roles; Enterprise plan for the Workspace owner role
ClickOps Implementation
Step 1: Structure Teams
- Create teams by function: navigate to Settings, scroll to Your teams, and click + (Join or create a team). Admins can also manage teams from Settings → Administration → Teams
- Restrict team creation to admins: Settings → Administration → Security → Restrict team creation
- Set each team’s visibility (see 2.2)
Step 2: Assign Minimum Necessary Roles
- Navigate to: Settings → Administration → Members
- Review the workspace roles:
- Workspace owner (Enterprise only): full control, including billing, security, audit logs, workspace exports, and OAuth application approvals
- Admin: routine workspace administration; on Enterprise plans, admins have more limited permissions than owners
- Team owner (Business and Enterprise): delegated control of an individual team
- Member: collaborates across the teams they can access; no workspace administration
- Guest (Business and Enterprise): access only to the teams they are explicitly added to
- Change a role from the member’s row: overflow menu (⋯) → Change role…
- Use Guests for contractors and external collaborators instead of Members
- With SCIM enabled, roles come from IdP groups (
linear-owners,linear-admins,linear-guests); manage them there - Review membership regularly. Suspend leavers from the row’s overflow menu (⋯) → Suspend user…; suspended users lose all access immediately
Step 3: Delegate Team Administration
- Promote team owners from Team settings → Members
- In Team settings → Access and permissions, restrict label, template, team-settings, and member management to team owners where appropriate
Code Implementation
Code Pack: API Script
Q_USERS='query HthLinearUsers($after: String) {
users(first: 250, after: $after) { nodes { active admin owner guest app } pageInfo { hasNextPage endCursor } }
}'
Q_TEAM_CREATION='query HthLinearTeamCreation { organization { securitySettings restrictTeamCreationToAdmins } }'
audit_roles() {
paginate "${Q_USERS}" "users"
local counts humans owners admins members guests apps role legacy
# TRAP 2: apps are counted apart from humans.
counts="$(printf '%s' "${PAGE_NODES}" | jq -c '
[ .[] | select(.active == true) ] as $a
| ($a | map(select(.app != true))) as $h
| { humans: ($h | length),
owners: ($h | map(select(.owner == true)) | length),
admins: ($h | map(select(.owner != true and .admin == true)) | length),
guests: ($h | map(select(.owner != true and .admin != true and .guest == true)) | length),
members: ($h | map(select(.owner != true and .admin != true and .guest != true)) | length),
apps: ($a | map(select(.app == true)) | length) }')"
humans="$(printf '%s' "${counts}" | jq '.humans')"
owners="$(printf '%s' "${counts}" | jq '.owners')"
admins="$(printf '%s' "${counts}" | jq '.admins')"
members="$(printf '%s' "${counts}" | jq '.members')"
guests="$(printf '%s' "${counts}" | jq '.guests')"
apps="$(printf '%s' "${counts}" | jq '.apps')"
echo "Linear 2.1: role distribution and team creation"
echo " active humans: ${humans} (owners ${owners}, admins ${admins}, members ${members}, guests ${guests}); app users: ${apps}"
if [ "${humans}" -eq 0 ]; then
undetermined "the users query returned no active human users, so the role split could not be assessed."
elif [ "${humans}" -gt 1 ] && [ "$((owners + admins))" -eq "${humans}" ]; then
# TRAP 1
finding "every active user holds an owner or admin role, so no one has least privilege (Linear's Free plan makes every user an admin)."
fi
gql "${Q_TEAM_CREATION}"
role="$(dq -r '(.organization.securitySettings // {}).teamCreationRole // ""')"
legacy="$(dq -r '.organization.restrictTeamCreationToAdmins | if . == null then "" else tostring end')"
if [ -n "${role}" ]; then
echo " securitySettings.teamCreationRole: ${role}"
case "${role}" in
owner|admin) ;;
user|guest) finding "teamCreationRole is '${role}': members below admin can create teams (\"Restrict team creation\" is off)." ;;
*) undetermined "teamCreationRole has an unrecognised value '${role}'." ;;
esac
elif [ -n "${legacy}" ]; then
echo " restrictTeamCreationToAdmins (deprecated mirror): ${legacy}"
case "${legacy}" in
true) ;;
false) finding "\"Restrict team creation\" is off: any member can create teams." ;;
*) undetermined "restrictTeamCreationToAdmins has an unrecognised value '${legacy}'." ;;
esac
else
undetermined "neither securitySettings.teamCreationRole nor restrictTeamCreationToAdmins is set, so the team-creation policy could not be read."
fi
}
Source: Members and roles, Teams
2.2 Configure Project Visibility
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 5.4 |
| NIST 800-53 | AC-6 |
Description
Control who can see sensitive work by making the teams that own it private. In Linear, issue and project visibility follows team membership: people who are not members of a private team cannot see its issues.
Rationale
Why This Matters:
- By default every workspace member can view and join any team that is not private, and search its issues, projects, and documents
- Private teams hide their issues from non-members, and projects created under a private team are visible only to its members
- Controlling cross-team access enforces need-to-know on the most sensitive work
- A compromised low-privilege account can only see what visibility settings expose to it
- Private-team data can still leave through the API, webhooks, and integrations: a personal API key of a private-team member can read that team’s issues, and team owners and admins can set up webhooks for private teams
Attack Prevented: Unauthorized data access, information disclosure, insider snooping
Prerequisites
- Business or Enterprise plan (private teams); Enterprise plan for sharing individual issues out of a private team
- Workspace owner, admin, or team owner access to change an existing team’s visibility
ClickOps Implementation
Step 1: Make Sensitive Teams Private
- For a new team, turn on Make team private when you create it from workspace settings
- For an existing team, open its team settings (right-click the team in the sidebar) → Access and permissions → Change team visibility
- Converting a team to private removes non-members from active issue assignments and unsubscribes non-member subscribers
- Admins can review every private team at Settings → Administration → Teams
Step 2: Restrict Who Can Join Public Teams
- In Team settings → Access and permissions, restrict team access to members that team owners add or invite
Step 3: Control Sub-Teams and Issue Sharing
- Under a private parent team, choose Private for any sub-team that must be hidden from the parent team’s members. Restricted, the default, lets members of the parent team see and join it
- On Enterprise, decide in Team settings → Access and permissions → Issue sharing whether members may share individual issues out of a private team
Step 4: Review Exposure Paths
- Review the API keys (3.2), webhooks, and integrations (3.1) held by members of private teams
- When exporting, remember that admins can include issues from any private team
Code Implementation
Code Pack: API Script
TEAM_FIELDS='nodes { id key visibility scimManaged allMembersCanJoin } pageInfo { hasNextPage endCursor }'
Q_TEAMS="query HthLinearTeams(\$after: String) { teams(first: 250, after: \$after) { ${TEAM_FIELDS} } }"
Q_ADMIN_TEAMS="query HthLinearAdminTeams(\$after: String) { administrableTeams(first: 250, after: \$after) { ${TEAM_FIELDS} } }"
SENSITIVE_TEAM_KEYS="${LINEAR_SENSITIVE_TEAM_KEYS:-}"
audit_team_visibility() {
local readable administrable teams key vis
paginate "${Q_TEAMS}" "teams"; readable="${PAGE_NODES}"
paginate "${Q_ADMIN_TEAMS}" "administrableTeams"; administrable="${PAGE_NODES}"
# TRAP 1: union by id, so private teams the key administers but cannot read are included.
teams="$(jq -nc --argjson a "${readable}" --argjson b "${administrable}" '$a + $b | unique_by(.id)')"
echo "Linear 2.2: team visibility"
printf '%s' "${teams}" | jq -r '
" teams visible to this key: \(length) (private \(map(select(.visibility == "private")) | length),"
+ " restricted \(map(select(.visibility == "restricted")) | length),"
+ " public \(map(select(.visibility == "public")) | length))",
" public teams any member can join: \(map(select(.visibility == "public" and .allMembersCanJoin == true)) | length)",
(sort_by(.key)[] | " - \(.key): \(.visibility)\(if .scimManaged then " (SCIM-managed)" else "" end)")'
if [ -z "${SENSITIVE_TEAM_KEYS}" ]; then
# TRAP 2
undetermined "LINEAR_SENSITIVE_TEAM_KEYS is not set. Name the teams that hold sensitive work (e.g. SEC,HR) to get a verdict."
return 0
fi
echo " declared sensitive teams: ${SENSITIVE_TEAM_KEYS}"
local IFS=','
for key in ${SENSITIVE_TEAM_KEYS}; do
key="$(printf '%s' "${key}" | tr -d '[:space:]')"
[ -n "${key}" ] || continue
vis="$(printf '%s' "${teams}" | jq -r --arg k "${key}" 'map(select((.key | ascii_upcase) == ($k | ascii_upcase))) | first | .visibility // ""')"
case "${vis}" in
private) ;;
public) finding "team ${key} is public. Every workspace member can view and join it, and search its issues, projects and documents." ;;
restricted) finding "team ${key} is restricted. Every member of its private parent team can see and join it; make it Private." ;;
"") undetermined "team ${key} was not visible to this key. It may be private and not administered by this user, or the key may be wrong. Re-run with an admin's or owner's key." ;;
*) undetermined "team ${key} has an unrecognised visibility '${vis}'." ;;
esac
done
}
Source: Private teams, Members and roles, Teams
2.3 Limit Admin Access
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 5.4 |
| NIST 800-53 | AC-6(1) |
Description
Keep the set of workspace owners and admins small and documented, and use Linear’s workspace restrictions to decide which role may perform sensitive workspace-level actions.
Rationale
Why This Matters:
- Owners and admins control sign-in methods, SAML, approved domains, API and integration settings, and membership, and they can sign in with any login method even when others are restricted. Each one is a high-value target
- Keeping admins to a small, documented set shrinks the attack surface for workspace takeover
- Protecting admin accounts with passkeys or IdP-enforced MFA protects the controls every other user depends on
- Monitoring admin activity surfaces unauthorized configuration changes quickly
- On the Free plan every user is an Admin, so this control can only be met on a paid plan
Attack Prevented: Workspace takeover, privilege abuse, unauthorized configuration changes
Prerequisites
- A paid plan (on the Free plan every user is an Admin)
- Enterprise plan and the Workspace owner role for workspace restrictions
ClickOps Implementation
Step 1: Inventory Admins and Owners
- Navigate to: Settings → Administration → Members and filter by role
- Any member can list the admins from the command menu (Cmd/Ctrl K) → View workspace admins
- Document why each person holds the role
Step 2: Reduce and Protect
- Limit admins and owners to 2-3 people
- Demote anyone else from the member’s row: overflow menu (⋯) → Change role… With SCIM, change membership of the
linear-adminsandlinear-ownersgroups in the IdP instead - Protect every owner and admin account with a passkey (1.2) or IdP-enforced MFA (1.1)
- Monitor admin activity in the audit log (4.1)
Step 3: Scope Sensitive Actions (Enterprise)
- As a workspace owner, navigate to: Settings → Administration → Security → Workspace restrictions
- Configure which roles may perform workspace-level actions. Linear’s API exposes these as minimum-role settings for inviting users, creating teams, creating personal API keys, managing API settings, installing integrations, and granting the admin role
Code Implementation
Code Pack: API Script
Q_USERS='query HthLinearPrivilegedUsers($after: String) {
users(first: 250, after: $after) { nodes { active admin owner app } pageInfo { hasNextPage endCursor } }
}'
Q_ADMIN_MGMT='query HthLinearAdminManagement { organization { securitySettings } }'
MAX_ADMINS="${LINEAR_MAX_ADMINS:-3}"
PRIVILEGED=0
audit_admin_count() {
case "${MAX_ADMINS}" in
''|*[!0-9]*|0) echo "PRECONDITION: LINEAR_MAX_ADMINS must be a whole number of 1 or more (got '${MAX_ADMINS}')" >&2; exit 2 ;;
esac
paginate "${Q_USERS}" "users"
local humans owners admins mgmt
humans="$(printf '%s' "${PAGE_NODES}" | jq '[ .[] | select(.active == true and .app != true) ] | length')"
owners="$(printf '%s' "${PAGE_NODES}" | jq '[ .[] | select(.active == true and .app != true and .owner == true) ] | length')"
admins="$(printf '%s' "${PAGE_NODES}" | jq '[ .[] | select(.active == true and .app != true and .owner != true and .admin == true) ] | length')"
PRIVILEGED=$((owners + admins))
echo "Linear 2.3: privileged accounts"
echo " active humans: ${humans}; owners: ${owners}; admins: ${admins}; privileged total: ${PRIVILEGED}; ceiling (LINEAR_MAX_ADMINS): ${MAX_ADMINS}"
gql "${Q_ADMIN_MGMT}"
mgmt="$(dq -r '(.organization.securitySettings // {}).adminManagementRole // "unset"')"
echo " securitySettings.adminManagementRole: ${mgmt} (informational)"
if [ "${humans}" -eq 0 ]; then
undetermined "the users query returned no active human users, so the privileged count could not be assessed."
elif [ "${PRIVILEGED}" -eq 0 ]; then
undetermined "no active human holds the owner or admin role, which a working workspace cannot have. Check the key's visibility."
elif [ "${PRIVILEGED}" -gt "${MAX_ADMINS}" ]; then
finding "${PRIVILEGED} active humans hold owner or admin (ceiling ${MAX_ADMINS}). Each one can change sign-in, SAML, API and membership settings, and bypasses login-method restrictions."
fi
}
Source: Members and roles, Login methods
3. Integration Security
3.1 Configure Integration Access
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 3.11 |
| NIST 800-53 | SC-12 |
Description
Control which third-party apps can be installed. Review the integrations, OAuth applications, and webhooks already connected, and verify the signature on every webhook your systems receive from Linear.
Rationale
Why This Matters:
- Each connected integration is granted access to workspace data and expands the supply-chain attack surface
- Removing unused integrations eliminates dormant OAuth grants that attackers can abuse if the vendor is compromised
- An app requests its OAuth scopes when it is authorized, and Linear documents no admin setting that narrows them afterward, so the effective control is deciding which apps get installed at all
- Integrations enabled for the workspace are reachable by guest users, which can expose data from teams the guest was never added to
- A webhook receiver that does not verify Linear’s signature accepts forged events from anyone who learns its URL
Attack Prevented: Supply-chain compromise, OAuth token abuse, data exfiltration via third parties, forged webhook events
Prerequisites
- Any paid plan for third-party application approvals
- Admin permissions to view Settings → Administration → API; on Enterprise, the workspace owner manages app approvals
ClickOps Implementation
Step 1: Require Approval for Third-Party Apps
- Navigate to: Settings → Administration → Security
- Turn on third-party application approvals. On Enterprise a workspace owner does this; on other paid plans a workspace admin does
- Members who try to install an app can then only request approval. Approved and denied apps appear in Applications
Step 2: Review Integrations, OAuth Apps, and Webhooks
- Navigate to: Settings → Integrations and remove integrations the workspace no longer uses
- Navigate to: Settings → Administration → API and review OAuth applications and webhooks. Delete any webhook without a current owner
- For Linear-built integrations (GitHub, GitLab, Figma, Sentry, Intercom, Zapier, Airbyte), make sure guest users have no access to your accounts on those services
Step 3: Verify Webhook Signatures
- Copy each webhook’s signing secret from the webhook’s detail page
- Every receiver must verify the
Linear-Signatureheader, a hex-encoded HMAC-SHA256 of the raw request body keyed with that secret, and reject any request whose signedwebhookTimestampis more than a minute old - Optionally, also accept only Linear’s published outbound IP addresses (
https://linear.app/.well-known/appspecific/app.linear.ips.json)
Code Implementation
The API pack audits the integration surface. The SDK pack is a webhook receiver that accepts only signed, fresh events, using Linear’s official SDK helper.
Code Pack: API Script
Q_INTEGRATIONS='query HthLinearIntegrations($after: String) {
integrations(first: 250, after: $after) { nodes { service createdAt } pageInfo { hasNextPage endCursor } }
}'
Q_WEBHOOKS='query HthLinearWebhooks($after: String) {
webhooks(first: 250, after: $after) { nodes { enabled url allPublicTeams resourceTypes label } pageInfo { hasNextPage endCursor } }
}'
Q_INTEGRATION_ROLE='query HthLinearIntegrationRole { organization { securitySettings } }'
APPROVED_INTEGRATIONS="${LINEAR_APPROVED_INTEGRATIONS:-}"
audit_integrations() {
local integrations webhooks unapproved insecure role
paginate "${Q_INTEGRATIONS}" "integrations"; integrations="${PAGE_NODES}"
paginate "${Q_WEBHOOKS}" "webhooks"; webhooks="${PAGE_NODES}"
echo "Linear 3.1: integrations, webhooks, and who may install integrations"
printf '%s' "${integrations}" | jq -r '
" integrations: \(length)",
(group_by(.service)[] | " - \(.[0].service): \(length) connection(s), oldest \(map(.createdAt) | min | .[0:10])")'
# TRAP 1: scheme://host only.
printf '%s' "${webhooks}" | jq -r '
" webhooks: \(length) (enabled \(map(select(.enabled == true)) | length))",
(.[] | " - \((.url // "(no url)") | capture("^(?<o>[A-Za-z][A-Za-z0-9+.-]*://[^/?#]*)").o // "(unparseable url)")"
+ " enabled=\(.enabled) allPublicTeams=\(.allPublicTeams) resources=\((.resourceTypes // []) | join(","))")'
insecure="$(printf '%s' "${webhooks}" | jq '[ .[] | select(.enabled == true and ((.url // "") | test("^https://"; "i") | not)) ] | length')"
if [ "${insecure}" -gt 0 ]; then
finding "${insecure} enabled webhook(s) deliver workspace data to a non-HTTPS URL."
fi
if [ -z "${APPROVED_INTEGRATIONS}" ]; then
# TRAP 3
undetermined "LINEAR_APPROVED_INTEGRATIONS is not set. Name the integration services you have approved to get a verdict on the list above."
else
unapproved="$(printf '%s' "${integrations}" | jq -r --arg ok "${APPROVED_INTEGRATIONS}" '
($ok | split(",") | map(ascii_downcase | gsub("^\\s+|\\s+$"; "")) | map(select(. != ""))) as $approved
| [ .[].service | select((ascii_downcase) as $s | ($approved | index($s)) == null) ] | unique | join(", ")')"
if [ -n "${unapproved}" ]; then
finding "integrations outside LINEAR_APPROVED_INTEGRATIONS are connected: ${unapproved}"
fi
fi
gql "${Q_INTEGRATION_ROLE}"
role="$(dq -r '(.organization.securitySettings // {}).integrationCreationRole // ""')"
case "${role}" in
owner|admin) echo " securitySettings.integrationCreationRole: ${role}" ;;
user|guest) echo " securitySettings.integrationCreationRole: ${role}"
finding "integrationCreationRole is '${role}': members below admin can install and connect new integrations." ;;
"") undetermined "securitySettings.integrationCreationRole is not set, so who may install integrations could not be read." ;;
*) undetermined "integrationCreationRole has an unrecognised value '${role}'." ;;
esac
# TRAP 2
echo " third-party application approvals: NOT CHECKED. Not readable through the public API; confirm it in Settings > Administration > Security."
}
Code Pack: SDK Script
const secret = process.env.LINEAR_WEBHOOK_SECRET;
if (!secret) {
// TRAP 2
console.error("PRECONDITION: set LINEAR_WEBHOOK_SECRET to the webhook's signing secret");
process.exit(2);
}
const port = Number(process.env.PORT || 8080);
// createHandler() checks the Linear-Signature HMAC over the raw body and the
// signed webhookTimestamp (±60 s), answering 400 on either failure and 405 to
// anything but POST. Only verified events reach the handlers registered below.
const handler = new LinearWebhookClient(secret).createHandler();
handler.on("*", async (payload) => {
// Replace with your processing. `payload` is already verified.
console.log(`verified ${payload.type} ${payload.action} webhook=${payload.webhookId}`);
});
http
.createServer((req, res) => {
if (req.url !== "/hooks/linear") {
res.statusCode = 404;
res.end();
return;
}
// TRAP 1: the raw IncomingMessage goes straight to the SDK, unparsed.
handler(req, res);
})
.listen(port, () => console.log(`listening on :${port}/hooks/linear`));
Source: Third-Party App Approvals, API and Webhooks, Webhooks, SDK webhooks, Members and roles, GitHub integration
3.2 Configure API Tokens
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 3.11 |
| NIST 800-53 | SC-12 |
Description
Restrict who may create personal API keys. Then review and prune everything that holds standing credentials to a Linear account: personal API keys, active sessions, and authorized OAuth applications.
Rationale
Why This Matters:
- Personal API keys act as long-lived credentials that bypass interactive SSO and login-method restrictions
- Keys leaked in code, logs, or CI configs grant direct programmatic access to workspace data
- Admins decide whether Members may create API keys at all, and can see and revoke every existing key in the workspace
- Authorized OAuth applications hold delegated access that persists after the tool stops being used, so an unrevoked grant is a live path into the workspace if the third party is breached
- Active sessions are recorded with location and date, which makes an unrecognized session the earliest visible sign of account takeover
- Documenting key purposes makes it possible to spot and revoke credentials that no longer have an owner
Attack Prevented: Credential leakage, token theft, unauthorized API access, OAuth grant abuse, persistent access via stale tokens and sessions
ClickOps Implementation
Step 1: Restrict and Review Workspace API Keys
- Navigate to: Settings → Administration → API
- Under Member API keys, choose whether Members may create their own API keys. Admins can always create them
- Review the workspace’s existing API keys on the same page and revoke any without a documented owner and purpose
Step 2: Review Personal API Keys
- Navigate to: Settings → Account → Security & Access
- Review every personal API key and document its purpose and owner
- Create automation keys with the narrowest permission that works (Read, Write, Admin, Create issues, Create comments), limited to specific teams where possible
- Revoke keys with no documented owner or purpose
Step 3: Revoke Stale Sessions
- On the same page, review active sessions. Each is listed with its location and date last seen, and clicking an entry shows its IP address and original sign-in date
- Revoke any session from an unrecognized location, an old device, or a departed contractor. Inactive sessions expire automatically after 30 days
- Treat an unexplained session as a suspected compromise: revoke it, then rotate the account’s API keys
Step 4: Review Authorized OAuth Applications
- Review the list of authorized OAuth applications on the same page
- Revoke every application the member no longer actively uses
- Re-review after any third-party breach disclosure affecting a connected vendor
When a user is suspended or converted to a guest, Linear revokes that user’s API tokens.
Code Implementation
Linear’s public GraphQL schema has no type that lists personal API keys, so reviewing existing keys (Steps 1–2) is console-only. The pack checks who may create keys and inventories the key owner’s own sessions.
Code Pack: API Script
Q_API_KEY_POLICY='query HthLinearApiKeyPolicy { organization { securitySettings } }'
Q_SESSIONS='query HthLinearSessions { authenticationSessions { createdAt lastActiveAt isCurrentSession type } }'
audit_api_keys_and_sessions() {
local role
gql "${Q_API_KEY_POLICY}"
role="$(dq -r '(.organization.securitySettings // {}).personalApiKeysRole // ""')"
echo "Linear 3.2: personal API key policy and sessions"
case "${role}" in
owner|admin) echo " securitySettings.personalApiKeysRole: ${role} (members cannot create personal API keys)" ;;
user|guest) echo " securitySettings.personalApiKeysRole: ${role}"
finding "personalApiKeysRole is '${role}': members below admin can mint long-lived personal API keys (\"Member API keys\" is on)." ;;
"") undetermined "securitySettings.personalApiKeysRole is not set, so who may create personal API keys could not be read." ;;
*) undetermined "personalApiKeysRole has an unrecognised value '${role}'." ;;
esac
# TRAP 1
echo " existing personal API keys: NOT CHECKED. No public query lists them; review Settings > Administration > API."
# TRAP 2: informational inventory of the key owner's sessions, with no IP or location.
gql "${Q_SESSIONS}"
dq -r '
def age_days: (now - (sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601)) / 86400 | floor;
.authenticationSessions as $s
| " active sessions of the key owner: \($s | length)"
+ " (by client: \($s | group_by(.type) | map("\(.[0].type)=\(length)") | join(", ")))",
(if ($s | length) > 0 then
" oldest session created \($s | map(.createdAt | age_days) | max) day(s) ago;"
+ " least recently active \($s | map((.lastActiveAt // .createdAt) | age_days) | max) day(s) ago"
else empty end)'
}
Source: API and Webhooks, Security & Access, Invite members
4. Monitoring & Compliance
4.1 Configure Audit Logs
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 8.2 |
| NIST 800-53 | AU-2 |
Description
Review Linear’s workspace audit log and stream it to your SIEM. Linear keeps audit events for only 90 days, so anything you need beyond that window has to be exported while it still exists.
Rationale
Why This Matters:
- The audit log is the workspace’s record of who did what. Each entry carries the actor’s IP address and country, which is what makes an anomalous login or a session from an unexpected jurisdiction visible
- Linear retains audit log events for 90 days only. An intrusion discovered on a typical breach-detection timeline can easily predate the oldest available evidence, and exporting to a SIEM is what preserves the trail
- Monitoring authentication, permission, and integration events surfaces account compromise and insider abuse early
- Retained audit records support SOC 2, GDPR, and HIPAA evidence requirements, which generally demand retention well beyond 90 days
Attack Prevented: Undetected intrusion, insider abuse, repudiation, delayed incident response, evidence loss through log expiry
Prerequisites
- Enterprise plan
- Workspace Owner role; the audit log is restricted to workspace owners
ClickOps Implementation
Step 1: Access the Audit Log
- Navigate to: Settings → Administration → Audit Log (workspace owners only)
- Review entries (actor, action, IP address, and country). You can filter by event type and hide session-creation events
- Confirm the 90-day retention window against your own evidence requirements
Step 2: Stream Logs to Your SIEM
- On the Audit Log page, enable Stream logs and configure the webhook endpoint
- Point the webhook at your SIEM’s HTTP collector so events are captured continuously rather than exported by hand
- Verify the webhook’s signature with its signing secret, as in 3.1, so forged events cannot be injected into your audit trail
- Verify events are arriving, then set retention in the SIEM to match your compliance obligation
Step 3: Query Programmatically
- The audit log is queryable through Linear’s GraphQL API (
auditEntries), filterable by type, actor, IP address, and date range;auditEntryTypeslists every event type - Use it for point-in-time investigation, and to reconcile SIEM coverage gaps before the 90-day window closes
Key Events to Monitor:
- Authentication events and sessions from unexpected countries
- Permission and role changes
- Integration and OAuth application changes
- Membership additions and removals
Code Implementation
Code Pack: API Script
Q_AUDIT='query HthLinearAuditEntries($since: DateTimeOrDuration!, $after: String) {
auditEntries(first: 250, after: $after, filter: { createdAt: { gte: $since } }) {
nodes { type createdAt } pageInfo { hasNextPage endCursor }
}
}'
WINDOW_DAYS="${LINEAR_AUDIT_WINDOW_DAYS:-7}"
audit_log_entries() {
case "${WINDOW_DAYS}" in
''|*[!0-9]*|0) echo "PRECONDITION: LINEAR_AUDIT_WINDOW_DAYS must be a whole number from 1 to 90 (got '${WINDOW_DAYS}')" >&2; exit 2 ;;
esac
if [ "${WINDOW_DAYS}" -gt 90 ]; then
echo "PRECONDITION: LINEAR_AUDIT_WINDOW_DAYS exceeds Linear's 90-day audit retention" >&2
exit 2
fi
local since="-P${WINDOW_DAYS}D" after="" more pages=0 vars count=0 page_count
: > "${TYPES_FILE}"
while :; do
vars="$(jq -nc --arg s "${since}" --arg a "${after}" '{since: $s, after: (if $a == "" then null else $a end)}')"
gql "${Q_AUDIT}" "${vars}"
dq -r '.auditEntries.nodes[].type' >> "${TYPES_FILE}"
page_count="$(dq '.auditEntries.nodes | length')"
count=$((count + page_count))
more="$(dq -r '.auditEntries.pageInfo.hasNextPage')"
after="$(dq -r '.auditEntries.pageInfo.endCursor // ""')"
pages=$((pages + 1))
case "${more}" in
false) break ;;
true) ;;
*) echo "PRECONDITION: auditEntries.pageInfo.hasNextPage came back as '${more}'" >&2; exit 2 ;;
esac
if [ -z "${after}" ] || [ "${pages}" -ge 400 ]; then
echo "PRECONDITION: pagination of auditEntries stopped before the last page" >&2
exit 2
fi
done
echo "Linear 4.1: audit log, last ${WINDOW_DAYS} day(s)"
echo " entries read: ${count}"
if [ "${count}" -eq 0 ]; then
# TRAP 1
undetermined "no audit entries in the last ${WINDOW_DAYS} day(s). An empty read is not evidence the log is recording; widen LINEAR_AUDIT_WINDOW_DAYS or generate a known event and re-run."
else
echo " top event types:"
sort "${TYPES_FILE}" | uniq -c | sort -rn | awk 'NR <= 10 {printf " - %s: %s\n", $2, $1}'
fi
# TRAP 2
echo " stream logs to SIEM: NOT CHECKED. Not readable through the public API; confirm under Settings > Administration > Audit Log."
}
Source: Audit log
4.2 Choose Data Residency at Workspace Creation
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 3.1 |
| NIST 800-53 | SA-9 |
Description
Linear lets you choose whether a workspace’s data is hosted in the US or the EU region. The choice is made at workspace creation and is not self-serve to change afterward, so regulated teams must decide before the workspace exists.
Rationale
Why This Matters:
- Data residency is effectively fixed in Linear. Pick the wrong region and the self-serve remedy is creating a new workspace and migrating the data, which is expensive and disruptive once issues, projects, and integrations have accumulated
- Teams subject to GDPR, national data-localization rules, or customer contracts with residency clauses need the EU region selected up front to keep issue content, attachments, and comments inside the required jurisdiction
- Issue trackers accumulate personal data incidentally (customer names in bug reports, screenshots, support transcripts), so residency obligations apply even when the workspace was never intended to hold regulated data
- Documenting which region a workspace runs in gives auditors and vendor-risk reviewers a verifiable answer instead of an assumption
Attack Prevented: Regulatory exposure from cross-border data transfer, residency-clause breach, unplanned data migration
Some data stays in the US whatever region you choose. Workspace information, all user account information, and user-created API keys (used to authenticate users and route them to the right region), notification emails (held 7 days by Linear’s email provider), usage and analytics data, and crash-related account information are always stored in the United States (Security, checked 2026-09-24). Record these exceptions in your data-processing inventory.
ClickOps Implementation
Step 1: Decide Region Before Creating the Workspace
- Confirm your residency obligation (GDPR, customer contract, internal policy) before anyone provisions the workspace
- Select the EU region at creation if any obligation requires EU hosting; otherwise select US
- Record the chosen region in your vendor inventory, together with the US-only data categories above
Step 2: Verify Existing Workspaces
- Navigate to: Settings → Administration → Workspace to see each existing workspace’s region
- Where a workspace is in the wrong region, plan a migration to a new, correctly provisioned workspace rather than expecting a setting change
- Document the outcome for audit evidence
Automation: ClickOps only. Linear exposes no write interface for this setting: the region is chosen when the workspace is created and “isn’t self-serve to change later” (Security, 2026-09-24).
Validation & Testing
Read the workspace’s region at Settings → Administration → Workspace and check that it matches the region recorded in your data-processing inventory. Since the setting is fixed at creation, validation is a one-time attestation rather than a recurring config check.
Source: Security
5. Compliance Quick Reference
SOC 2 Trust Services Criteria Mapping
| Control ID | Linear Control | Guide Section |
|---|---|---|
| CC6.1 | SSO and login-method restriction | 1.1 |
| CC6.2 | Team permissions | 2.1 |
| CC7.2 | Audit logs | 4.1 |
NIST 800-53 Rev 5 Mapping
| Control | Linear Control | Guide Section |
|---|---|---|
| IA-2 | SSO | 1.1 |
| IA-2(1) | Login-method restriction | 1.2 |
| AC-6 | Team permissions | 2.1 |
| AU-2 | Audit logs | 4.1 |
Appendix A: References
Official Linear Documentation:
- Linear Documentation
- Login methods
- SAML
- SCIM
- Invite members
- Members and roles
- Teams
- Private teams
- API and Webhooks
- Third-Party App Approvals
- Security
- Security & Access
- Audit log
API & Developer Resources:
- Linear Developers
- GraphQL API: getting started
- Webhooks
- TypeScript SDK
- SDK webhook helper
- GraphQL schema (SDL)
Compliance Frameworks:
- SOC 2 Type II, GDPR, HIPAA (Enterprise plan with BAA). See Linear Security documentation
Security Incidents:
- No major public security breaches identified as of this writing.
Changelog
| Date | Version | Maturity | Changes | Author |
|---|---|---|---|---|
| 2026-09-25 | 0.3.0 | ai-drafted | validate-hth-guide run (Phases 4–5, read-only tenant policy). 0 surfaces exercised live: the Linear console was behind a sign-in wall in the validation browser and no API key could be minted, so maturity is unchanged. Corrections from current Linear docs: rewrote 1.2 around Restrict login methods (Linear has no two-factor setting; renamed from “Enforce Two-Factor Authentication”); fixed console paths throughout (Settings → Administration → …); 1.1 now describes metadata exchange, domain claiming and SCIM; 1.3 reframed approved domains as auto-join rather than a block and added invitation controls; 2.1 lists all five roles and the Free-plan all-admin caveat; 2.2 gains real paths for private teams; 2.3 adds workspace restrictions; 3.1 adds third-party app approvals and webhook signature verification; 3.2 adds the workspace Member API keys setting; 4.2 lists the data that always stays in the US; replaced the stale developers.linear.app links. Added nine read-only GraphQL audit packs and one SDK webhook-verification pack, all run against a schema-validating mock and failing closed against the real API; 4.2 carries a ClickOps-only automation verdict | Claude Code (Opus 5.5) |
| 2026-08-08 | 0.2.0 | ai-drafted | Currency pass, scope-limited: only three Linear documentation pages (security, security-and-access, audit-log) were publicly reachable this pass, so findings outside them soften rather than assert. Rewrote 4.1 audit log (Enterprise, workspace owners only, 90-day retention, GraphQL queryable, webhook streaming to SIEM, actor IP/country); added 4.2 data residency (US/EU, fixed at workspace creation); added passkeys to 1.2 and OAuth-grant/session review to 3.2; annotated 1.2 and 1.3 as unverifiable against current public docs; removed the 404 SAML SSO link and the Trust Center/security marketing links in favor of linear.app/docs/security | Claude Code (Opus 4.8) |
| 2026-06-29 | 0.1.1 | ai-drafted | Add cheat-sheet Description and Rationale for all controls | Claude Code (Opus 4.8) |
| 2025-02-05 | 0.1.0 | ai-drafted | Initial guide with SSO, teams, and integrations | Claude Code (Opus 4.5) |
Contributing
Found an issue or want to improve this guide?
- Report outdated information: Open an issue with tag
content-outdated - Propose new controls: Open an issue with tag
new-control - Submit improvements: See Contributing Guide