This project exists to keep Linear aligned with Snyk.
Canonical module path:
github.com/RichardoC/snyk-linear-sync
The intended outcome is:
- every relevant Snyk finding has a corresponding Linear issue
- the Linear issue stays current as the Snyk finding changes
- resolved or ignored findings move to the correct Linear workflow state
- repeated runs become cheap by skipping unchanged records
This is an operational sync tool, not a generic library.
The project treats Snyk as the source of truth for security finding data and Linear as the execution surface for tracking and triage.
It is responsible for:
- reading Snyk findings
- deciding the desired Linear representation
- reconciling the current Linear state to that desired state
It is not responsible for:
- writing back to Snyk
- preserving arbitrary manual edits inside the managed section of the Linear description
- syncing comments or custom fields beyond the currently managed issue body, title, priority, due date, workflow state, and managed automation label
Each Snyk finding is identified by:
snyk:<project-id>:<issue-id>
That fingerprint is embedded in the Linear description metadata block and is the durable join key between systems.
Without that fingerprint, the sync cannot safely deduplicate or update issues.
Create a Linear issue when:
- a Snyk finding exists
- no Linear issue with the same fingerprint exists
Update the Linear issue when managed fields differ:
- title
- description
- due date
- priority
- mapped state
- managed automation label
When a previously tracked finding no longer exists in Snyk but its Snyk project still exists, move the Linear issue to the resolved state.
When a previously tracked finding no longer exists because its Snyk project no longer exists, cancel the Linear issue instead.
If multiple Linear issues share the same fingerprint, the sync treats that as a conflict:
- it logs the conflict
- it skips automatic mutation for that fingerprint
The Linear issue description is intentionally structured for fast triage first, deep debugging second.
It includes:
- heading with vulnerability title and severity
- repository context near the top
- branch/reference and commit context when available
- source file or project target file context near the top
- human-usable Snyk UI link
- Snyk REST API link
- package/version details
- fix availability, CVSS, CWE class(es), and CVE identifier(s) when Snyk reports them
- vulnerability description and remediation guidance sections when Snyk reports them
- project identifiers
- issue identifiers
- GitHub repository links when
SOURCE_PROVIDER=github - GitHub source file and commit links when
SOURCE_PROVIDER=github - GitHub project target file links when
SOURCE_PROVIDER=githuband no precise source location is available - metadata block
The synced title is also structured for scanability. It includes the best available source context:
- repository for code and repository-backed findings
- branch/image/reference for non-GitHub target-file findings such as Kubernetes or container scans
The subject portion then uses this preference:
- source file basename
- package name
- project target file
- project name
The metadata block also records the managed automation label name when label management is enabled.
Linear may rewrite parts of the description body when rendering or storing markdown. The sync therefore normalizes known Linear formatting changes during compare and cache hashing.
SOURCE_PROVIDER controls how source references are rendered.
unknownleaves source references as plain textgithubrenders public GitHub links for:- repositories
- source files, pinned to the reported commit
- source commits
- project target files, pinned to the reported branch/reference when no source commit/file is available
If repository, file, or commit data is missing, the sync falls back to plain text.
LINEAR_MANAGED_LABEL controls the label this tool manages on synced issues.
- default:
snyk-automation off: disables label management- any other value: the exact Linear label name to manage
Behavior:
- the configured managed label is added to new synced issues
- unrelated existing labels are preserved
- if the configured managed label changes, the old managed label is removed and the new one is applied
- if label management is disabled, the previously managed label is removed
The configured label must already exist in Linear. If it does not, the run fails with a clear operator-facing error.
LINEAR_TOOL_LABELS optionally maps Snyk issue type values to additional managed Linear labels.
- format: comma-separated
issue_type:labelpairs - example:
code:snyk-code,package_vulnerability:snyk-open-source
LINEAR_TOOL_LABEL_DEFAULT controls the fallback label for unmapped issue types.
- default:
snyk-automation off: disables the fallback
LINEAR_ORIGIN_LABELS optionally maps Snyk project origin values to additional managed Linear labels.
- format: comma-separated
origin:labelpairs - example:
github:snyk-github,kubernetes:snyk-kubernetes
LINEAR_ORIGIN_LABEL_DEFAULT controls the fallback label for unmapped project origins.
- default:
off off: disables the fallback
LINEAR_AWAITING_FIX_LABEL controls the label applied to issues where Snyk reports the ignore as "until fix is available" (disregardIfFixable).
- default:
triage-dependency off: disables the awaiting-fix label- any other value: the exact Linear label name to apply
- the label must already exist in Linear
When an issue is in the awaiting-fix state, the sync adds the configured label. When the fix becomes available and the issue moves to Todo, the label is removed. The label is tracked in the metadata block alongside tool and origin labels.
The metadata block stores the full managed label set so the sync can remove stale tool-derived, origin-derived, and awaiting-fix labels while preserving unrelated manual labels.
LINEAR_UNSUBSCRIBE_ACTOR is an optional operator control for notification behavior.
- default:
true - when enabled, the sync creates issues without subscribing the Linear API actor
- update operations preserve the current subscriber list exactly as it already exists
This only affects the persisted subscriber list. Linear still records the API user as the issue creator, which is separate from subscribers and can make the UI look like the creator is still "following" the issue even when the stored subscriber list is empty.
The current workflow mapping is:
open->Todoignored until fix available(disregardIfFixable) ->Backlog(no due date,triage-dependencylabel)temporarily ignored(snoozed with future expiry) ->Todopermanently ignored->Cancelledfixed->Done- missing finding in an existing active Snyk project ->
Done - missing finding because the Snyk project no longer exists ->
Cancelled - Snyk project is inactive (de-activated) ->
Cancelled
The sync also normalizes workflow naming differences such as Canceled vs Cancelled.
The ignored until fix available state is distinct from other ignored states because the ignore is conditional on an upstream fix. These issues are placed in Backlog with no due date and the triage-dependency label (configurable via LINEAR_AWAITING_FIX_LABEL). The description renders Status: ignored (no fix available) so the Snyk-side reality is visible in Linear. When a fix becomes available, Snyk flips ignored=false and the next run moves the issue to Todo with a recalculated due date.
When a user manually moves a managed open ticket from Todo to Backlog in Linear, subsequent syncs preserve the Backlog state instead of overriding it back to Todo. This prevents the automation from fighting intentional user triage decisions. The override matches the configured LINEAR_STATE_BACKLOG value with case-insensitive normalization.
When a user manually moves a managed issue to any non-terminal Linear workflow state that differs from what the sync would set, subsequent syncs preserve the user's chosen state. This generalises the previous Backlog-only and hardcoded "Todo" overrides.
This covers:
- a user moving an open issue from the configured open state (e.g.
Triage) toTodoorIn Progress - a user moving an awaiting-fix issue from
BacklogtoTodo - any other non-terminal state that differs from the configured state for the desired model state
Terminal states (Done, Cancelled) are never preserved: the sync always transitions to a terminal state when the Snyk finding is fixed or ignored, even if a user manually moved the issue to a terminal state.
Due dates are normally derived from the Snyk issue creation timestamp, not from when the issue first appears in Linear. For temporarily ignored issues, the due date is instead calculated from the ignore expiry date so the SLA extends to the normal severity offset from when the ignore expires.
Issues ignored "until fix is available" have no due date. The SLA clock is paused while the issue is blocked on an upstream fix. When a fix becomes available, Snyk flips ignored=false and the next run recalculates the due date from fix availability (today + severity offset), moving the issue to Todo.
Default offsets:
- critical: 15 days
- high: 30 days
- medium: 45 days
- low: 90 days
The project is designed for thousands of issues.
It uses:
- concurrent Snyk and Linear snapshot loading
- worker-based reconciliation
- batched Linear mutations
- rate-limit backoff
- SQLite caching
The cache is critical for steady-state performance. A healthy steady-state run should do little or no work when nothing has changed.
The SQLite cache stores:
- Snyk-side normalized hashes keyed by fingerprint
- Linear-side normalized hashes keyed by fingerprint
- a schema signature for the managed issue format
The cache is used only as an optimization. It should never be the only source used to infer real current state.
Normal behavior:
- Read live Snyk data.
- Read live Linear data.
- Compare both against the last successful cached hashes.
- Skip unchanged fingerprints.
- After successful writes, refresh the cache from the live post-write Linear snapshot.
The post-write refresh matters because Linear may rewrite markdown bodies after mutation.
- The metadata block must remain intact.
- The managed description body is owned by this tool.
- Linear issue history matters, so deleting and recreating all issues is a last resort, not a normal repair path.
- Cache bypass is the correct operator action when the rendering schema or compare logic changes.
After any code change, run the full verification suite (see README for the rationale behind the ordering):
go mod tidy && git diff --exit-code go.mod go.sum
go fix ./... && git diff --exit-code
go fmt ./...
go build ./...
go vet ./...
go test ./...Use a normal run for day-to-day sync:
go run ./cmd/snyk-linear-sync --env-file .envUse a dry run to inspect planned changes:
go run ./cmd/snyk-linear-sync --env-file .env --dry-runUse cache bypass when you intentionally changed the managed rendering or need a full live reconciliation:
go run ./cmd/snyk-linear-sync --env-file .env --bypass-cacheIf this project grows further, the next reasonable extensions would be:
- stronger incremental Snyk fetching using server timestamps where available
- more selective Linear snapshot loading if the API surface allows it safely
- richer cache statistics and observability
- explicit conflict reporting output
The current implementation is intentionally optimized for correctness first, then steady-state efficiency.