linstash is a single-executable Go CLI that moves selected Linear issues to
a self-hosted Kan instance, verifies the copy,
and archives the source issues through Linear's API.
I'm broke, so I built this because I love Linear.
Linear is excellent for active work, while its free plan has a finite active
issue allowance. Kan is a useful self-hosted home for work that no longer
needs to occupy the active Linear workspace. linstash connects the two
without running a server, daemon, webhook receiver, or scheduler.
| Linear | Kan |
|---|---|
| Workspace | Workspace |
| Team | Board |
| Workflow status | List |
| Project | Label with the exact project name |
| Issue | Card |
| Issue label | Not copied |
| Sub-issue | Card with parent/child metadata |
Projects that span multiple Linear teams receive the same project label on each corresponding Kan board.
The setup installers support macOS, Linux, and Windows on Intel/AMD (amd64)
and ARM (arm64). They download the latest GitHub release, verify its SHA-256
checksum, install linstash, and can immediately configure credentials and
the Linear/Kan mapping.
macOS or Linux:
curl -fsSL https://raw.githubusercontent.com/luberius/linstash/main/scripts/install.sh | shWindows PowerShell:
irm https://raw.githubusercontent.com/luberius/linstash/main/scripts/install.ps1 | iexThe installer asks whether to configure now. If accepted, it securely prompts
for both credentials, persists them for the current user, runs interactive
linstash configure, and finishes with linstash doctor. Canceling the
configuration keeps the installation and saved credentials without running
doctor against a missing configuration.
On macOS/Linux, make sure the default installation directory is on your
PATH:
export PATH="$HOME/.local/bin:$PATH"
linstash versionTo choose another directory or pin a version, set
LINSTASH_INSTALL_DIR or LINSTASH_VERSION before running the installer:
curl -fsSL https://raw.githubusercontent.com/luberius/linstash/main/scripts/install.sh |
LINSTASH_INSTALL_DIR="$HOME/bin" LINSTASH_VERSION="v1.0.0" shYou can also download the binary for your operating system and architecture
from the GitHub release and put it on your PATH.
You need:
- a Linear API key for the workspace you want to off-ramp;
- a running Kan instance and API token;
- permission to read and archive the selected Linear issues;
- permission to create the configured workspace structure and cards in Kan.
Set credentials in your shell. They are read from the environment and are never written to the YAML configuration:
export LINEAR_API_KEY="lin_api_..."
export KAN_API_TOKEN="..."Run the interactive configuration:
linstash configureconfigure discovers your Linear teams, projects, and statuses, asks how Kan
boards should be mapped, then previews every destination structure. Choose
team_as_board for the V1 layout, or project_as_board when Linear projects
should become boards and teams should become labels. In project mode you can
also explicitly include projectless issues. They are excluded by default;
choosing Yes reveals a team selector and routes each selected team's
projectless issues to that team's own <Team>'s board. Nothing is created
until you confirm the preview.
Interactive terminals use arrow-key selectors: ↑/↓ moves, Space toggles
items in multi-select lists, and Enter confirms. This applies to Kan
workspaces, mapping modes, projects, teams, statuses, tree policies, and import
destinations. Redirected input uses an equivalent numbered fallback.
Remote discovery and planning show a compact animated status symbol in a real
terminal. Fast requests avoid flicker, and redirected output remains plain and
stable for scripts.
Check the resulting connection and mappings:
linstash doctorAfter initial setup, extend the existing off-ramp policy without repeating the full wizard:
linstash configure add project
linstash configure add team
linstash configure switch workspaceadd project lists only Linear projects not already configured. add team is
available in project_as_board mode and adds projectless fallback boards for
teams not already configured. Both commands preserve the current workspace,
mapping mode, statuses, tree policy, and existing selections; they preview and
reconcile only the added topology. Confirming the project or team selection is
the write authorization, so incremental add commands do not ask for a second
confirmation.
If the existing status selection matches no workflow status for a newly
affected team, the add command stops and asks you to rerun full configure to
change that policy.
If an existing configured project or projectless team is no longer
discoverable in Linear, incremental configure warns that the stale UUID remains
in the local policy, then continues with the independent addition. Rerun full
configure when you want to reconcile removed Linear objects.
configure switch workspace keeps the Linear workspace, mapping mode, stash
group, status selection, and issue-tree policy while rebinding the destination
to another existing or newly created Kan workspace. After confirmation it
rebuilds destination mappings in a fresh state.db and retains the previous
config and state in a timestamped sibling backup. A failed switch restores the
original local files automatically.
Preview exactly what will move. plan is read-only:
linstash plan
linstash plan --group completed-project-workOff-ramp the selected issues after reviewing the plan:
linstash offrampofframp is an alias for stash; both commands copy issues into Kan, read
them back for verification, and only then archive the Linear source:
linstash stashBoth commands ask for confirmation. Use --yes only for an intentional
non-interactive run:
linstash offramp --group completed-project-work --yesReview completed or incomplete attempts:
linstash status
linstash status --jsonRead the full manual after installation:
man linstashIf the user man directory is not yet on MANPATH, print the same roff manual
directly with linstash man. The macOS/Linux installer writes it to
~/.local/share/man/man1/linstash.1 by default; set LINSTASH_MAN_DIR to
override that location.
To copy ordinary Kan boards into Linear without restoring or synchronizing stashed issues, run:
linstash importEach run asks for the Kan boards and one existing Linear team. Boards map to
projects, lists map to exact-name workflow statuses, labels map to issue
labels, and cards map to issues. Name conflicts and missing status types are
resolved interactively. The importer confirms each issue's stable identity and
routing fields, records local idempotency state, and adds a linear-import:
metadata comment to Kan. It
never deletes Kan data, creates Linear teams, or unarchives Linear issues. If
an individual Kan attachment cannot be accessed, the issue still imports: the
CLI warns and adds one nonduplicating Linear comment identifying the file that
was not copied. A Linear file-upload failure still fails that card. While
applying the import, the CLI shows the current board and overall card progress.
V1.1.2 is the current release. It adds recoverable Kan workspace switching, stable-field import verification, archived-project mapping recovery, and visible import progress. V1.1 disposable live testing passed durable renamed-project reuse in a fresh process, card-level recovery, private attachment copy, inaccessible-attachment warnings, exact managed-marker counts, and unchanged no-op reruns. All six published binaries and the public installer passed checksum verification.
To keep configuration and local state in a specific location, pass the same explicit config path to every command:
linstash --config "$HOME/.config/linstash/config.yaml" configure
linstash --config "$HOME/.config/linstash/config.yaml" doctor
linstash --config "$HOME/.config/linstash/config.yaml" plan
linstash --config "$HOME/.config/linstash/config.yaml" offrampimport is a separate one-way copier. It is not an on-ramp, restore, or
two-way synchronization feature.
linstash configure Discover Linear/Kan and write an explicit configuration
linstash doctor Validate credentials, configuration, and remote mappings
linstash plan Preview the exact issue trees that would be stashed
linstash stash Copy, verify, and archive the selected Linear issues
linstash offramp Alias for stash
linstash status Report prior migrations and incomplete attempts
linstash import Copy selected Kan boards into one existing Linear team
linstash man Print the linstash(1) manual page
stash and its offramp alias display the same plan and ask once for
confirmation. --yes supports intentional non-interactive use. Running either
command is the only trigger: linstash does not install or operate a
background process.
A stash group is a local named off-ramp policy, not a Linear or Kan object. It
records which Linear projects may move, which issue statuses qualify, and how
parent/child issue trees are handled. Configure currently creates one group
named completed-project-work automatically; the name matters only when a
command explicitly uses --group.
The status-category menu exposes five broad categories: backlog,
unstarted, started, completed, and canceled. Those menu labels are
built into linstash, but the actual workflow statuses are discovered live from
Linear. Selecting Completed, for example, selects every live status in the
relevant teams whose Linear category is completed, such as Done or
Shipped. Live statuses with other types, such as duplicate, remain
available through the individual-status selector.
Configure may also offer Select additional individual statuses (optional).
Those choices come directly from the selected projects' live Linear teams and
are displayed by status and team name. Use them only to include a specific
status outside the selected categories; no UUID entry is required.
A generated group defaults to completed statuses and the safest tree policy:
version: 1
linear:
workspace_id: "replace-with-linear-workspace-uuid"
workspace_slug: "example-workspace"
kan:
base_url: "https://kan.example.com"
workspace_public_id: "resolved-by-linstash-configure"
workspace_name: "Example Archive"
workspace_slug: "example-archive"
mapping_mode: "project_as_board"
groups:
- name: "completed-project-work"
project_ids:
- "replace-with-linear-project-uuid"
project_names:
replace-with-linear-project-uuid: "Cached project name"
orphan_team_ids:
- "optional-team-uuid-for-projectless-issues"
orphan_team_names:
optional-team-uuid-for-projectless-issues: "Cached team name"
statuses:
types: ["completed"]
ids: []
tree_policy: "require_whole_tree"Available tree policies are:
require_whole_tree: skip the tree unless every node matches.stash_whole_tree: a matching root selects all descendants.matching_nodes_only: stash only nodes that match the status selection.
- Normal
stashruns never create workspaces, boards, or lists implicitly. configurepreviews remote structures before creating them.- Every Kan card carries the immutable Linear issue UUID and a content checksum.
- The full selected tree must be copied and read back successfully before any Linear issue is archived.
- Source issues are archived with Linear's reversible
issueArchivemutation, never deleted. - A failed or partial Kan write leaves the Linear source unarchived.
- Comments retain original author and timestamp.
- V1 off-ramp preserves Linear attachment URLs but does not download their bytes. The separate V1.1 Kan import copies accessible attachment bytes into Linear private storage.
V1 is complete. The workflow has automated unit, adapter, state, and workflow coverage, and the disposable Linear/Kan journey passed configure, read-only planning, verified copy, leaves-before-parent archive, no-op rerun, unarchive recovery, and live failure probes. See the redacted V1 acceptance record.
Requirements:
- Go 1.25 or newer
go test ./...
go vet ./...
CGO_ENABLED=0 go build ./cmd/linstash
./scripts/check-project-docs.sh
make releaseStart at docs/README.md. The product contract, architecture,
technical design, and V1 acceptance criteria are maintained there.