stift.sh — technical reference hosted cloud · or self-host · mit core app.stift.sh — sign in github

stift.

Cloud storage for AI coding agent sessions. Your sessions — full conversation history, context, todo state — live in scattered dot-directories on whatever machine you happened to be using. stift gives them a home: log in once and a background service syncs every session automatically — no manual push/pull. Start free on Stift Cloud, or self-host the single MIT binary — same engine.

hosted or self-host one binary pure go stdlib der stift · „the pencil"
install the client (both paths)
$ curl -fsSL https://stift.sh/install.sh | sh

How it works

one server, every machine in sync

Every dev machine logs in once with stift login. That starts a background service which watches your agents' session directories and syncs them to the server, and restores sessions other machines have pushed. The server is either Stift Cloud (hosted) or a single MIT binary you run yourself. Same engine, same client.

laptop desktop new machine stift server stift daemon · claude, codex, … stift daemon · cursor, gemini, … stift login → sessions restored Stift Cloud, or self-hosted binary sessions · tar.gz + metadata skills & agent config · versioned tokens · hashed, per machine background sync, every 30s push / pull on demand
fig. 1 — every machine syncs with one server, in the background

Zero runtime dependencies (pure Go stdlib), no database server — sessions are stored as tar.gz blobs plus JSON metadata on disk. The same binary is both server (stift serve) and client (stift push / pull / …).

Supported agents

AgentNameWhat gets synced
Claude Codeclaude~/.claude/projects/<project>/<session>.jsonl + todo state
OpenAI Codex CLIcodex~/.codex/sessions/…/rollout-*.jsonl
Gemini CLIgemini~/.gemini/tmp/<project>/ (logs, saved chats, checkpoints)
Cursor CLIcursor~/.cursor/chats/<project>/<session>/
opencodeopencodesession + messages + parts from ~/.local/share/opencode/storage
aideraider.aider.chat.history.md, .aider.input.history (in-project)

Anything not listed can be added as a custom agent — a name and a path pattern in a small JSON file.

Two ways to get the server half. Let us run it — Stift Cloud, hosted and free to start — or run the binary yourself (self-host). The CLI and everything below are identical either way; only the URL you stift login to changes.

Stift Cloud

managed sync — sign up, get a token, push

Don't want to run a server? Stift Cloud is the same open-source engine, hosted for you on sync.stift.sh. Sign up, create a token in the dashboard, and point the CLI at it — sessions sync across your machines and your whole team. No infra, no upgrades, backups handled.

Start free — app.stift.sh no card to start · free tier · cancel anytime

Quickstart

sh
# 1. install the CLI
$ curl -fsSL https://stift.sh/install.sh | sh

# 2. sign up at app.stift.sh, create a token, then log in
#    (this also starts background auto-sync)
$ stift login https://sync.stift.sh --token stf_...

# 3. that's it — sessions sync automatically and show up in your dashboard.
#    push by hand any time you want:
$ stift push

Pricing

Self-host
$0 · MIT
run the binary yourself
  • Unlimited sessions & storage
  • Your server, your data
  • Single binary, no database
  • Community support
Read the guide
Cloud Free
$0 / mo
hosted, for getting going
  • Up to 100 sessions
  • Uncapped storage
  • Web dashboard
  • Zero infrastructure
Start free
Cloud Pro
$4 / seat / mo
hosted, for teams
  • Unlimited sessions
  • Uncapped storage
  • Team organizations
  • Per-seat billing, upgrade anytime
Start free

Storage is uncapped on every plan. Start on the free tier and upgrade in the dashboard when you outgrow it — billing is handled by Stripe.

Install the client

linux · macos · amd64 / arm64
sh
$ curl -fsSL https://stift.sh/install.sh | sh

The script (read it first) detects your OS/arch, verifies the binary's SHA-256 checksum, and installs to /usr/local/bin if writable, otherwise ~/.local/bin.

OverrideMeaning
STIFT_VERSIONversion to install (default latest)
STIFT_INSTALL_DIRwhere to put the binary
STIFT_BASE_URLdownload base (default https://stift.sh/dl)

Windows: download stift-windows-amd64.exe and put it on PATH. Or build from source with make build (Go 1.26+).

Direct downloads

PlatformBinaryChecksum
Linux x86-64stift-linux-amd64.sha256
Linux arm64stift-linux-arm64.sha256
macOS Intelstift-darwin-amd64.sha256
macOS Apple Siliconstift-darwin-arm64.sha256
Windows x86-64stift-windows-amd64.exe.sha256

Self-host a server

binary · systemd · proxmox lxc · docker — one minute each · or use Stift Cloud

Binary

sh
$ stift serve --data /var/lib/stift

On first start the server prints an admin token once — store it. Then it listens on :8580. For a permanent install on a Linux host (bare metal, VM, or LXC), use the hardened systemd unit from deploy/stift.service:

sh · as root
$ cp dist/stift-linux-amd64 /usr/local/bin/stift && chmod +x /usr/local/bin/stift
$ cp deploy/stift.service /etc/systemd/system/
$ systemctl enable --now stift
$ journalctl -u stift   # first-boot admin token is in here

Proxmox VE

One command on the Proxmox host creates an unprivileged Debian LXC (1 core / 512 MB / 8 GB by default), installs stift as a hardened systemd service, and prints the server URL + admin token, ready for stift login:

sh · on the proxmox host, as root
$ bash -c "$(curl -fsSL https://stift.sh/proxmox.sh)"

The script (read it first) shows its plan and asks for confirmation before creating anything. Flags: --ctid, --hostname, --storage, --disk, --cores, --memory, --bridge, --ip CIDR --gw IP (static instead of DHCP), --binary PATH (install a local build instead of downloading), --token, --yes. See --help.

Docker

sh
$ docker compose up -d        # uses docker-compose.yml in the repo
$ docker compose logs stift   # grab the first-boot admin token

Or pin the token instead of fishing it out of logs:

sh
$ export STIFT_ADMIN_TOKEN="stf_$(openssl rand -hex 24)"
$ docker run -d -p 8580:8580 -v stift-data:/data \
    -e STIFT_ADMIN_TOKEN stift:latest

A read-only web UI for browsing and downloading sessions is served at / (paste a token; it never leaves your browser).

Background sync

log in once — sessions sync on their own

stift login also starts a small background service, so you set up a machine once and then forget about it. A lightweight daemon — a per-user systemd/launchd service, or a detached process where neither exists — runs every ~30s and keeps everything in sync. No manual push/pull.

sh · once per machine
$ stift login https://sync.stift.sh --token stf_...
background auto-sync started — your sessions now sync automatically
  • Pushes every changed agent session across all your projects, continuously.
  • Pulls sessions for projects you're actively working on here, restoring them so the local agent sees them — never overwriting a live local file (conflicts are logged, not applied).
  • Projects are matched across machines by git repo name (the remote's last path segment, else the folder name), so the same repo lines up even when its path differs machine to machine.

On a second machine

Point stift at a folder and it pulls that project's history right away; later sessions keep syncing automatically.

sh · in the project directory
$ cd ~/code/myapp
$ stift link
linked ~/code/myapp -> myapp
pulled 12 session(s)

Managing the service

CommandWhat it does
stift start · stop · restartcontrol the background sync service
stift statusrunning state + any sessions on the server not restored here yet
stift link · unlink · linksmanage which folders pull which project
stift pull --project-id NAMErestore a whole project into the current directory
stift login --no-daemonlog in without starting background sync
Everything in Push & pull still works by hand — the daemon just does it for you. Restores never clobber a running agent's files.

Push & pull

manual control, when you want it
sh
# one-time, on each machine
$ stift login https://sessions.example.com --token stf_...

# see what agents/sessions exist for the current project
$ stift agents

# push this project's sessions (all agents) to the server
$ stift push

# ...or be specific
$ stift push --agent claude,codex --latest
$ stift push --all-projects        # everything on this machine

# on another machine: browse and restore
$ stift list
$ stift pull 1920b89e              # by id (prefixes work)
$ stift pull --latest --agent claude
  • Pushes are idempotent — re-pushing an unchanged session is a no-op (unchanged); a changed session updates the existing record in place (updated).
  • Pulls never overwrite existing local files unless you pass --force; --dry-run lists the archive contents first.
  • Sessions restore to the same project path they came from — agents key their session storage by project path, so this is what makes the agent see them.

Skills & agent config

your skills, subagents, commands and CLAUDE.md — versioned, on every machine
sh
# push this machine's skills, agents, commands and CLAUDE.md
$ stift push --skills

# on another machine
$ stift pull --skills
$ stift skills list

# every unit has its own history
$ stift skills history skills/deploy
$ stift skills diff skills/deploy
$ stift skills rollback skills/deploy 3

# admins: share a set with the whole team
$ stift push --skills --scope org
  • One unit, one version. Each skill, agent, command and CLAUDE.md is versioned on its own — rolling one back never touches another.
  • Three scopes. user (~/.claude/…), project (.claude/…) and org — written by admins, pulled by everyone into ~/.stift/org and linked in.
  • No surprises. A push on top of a version you haven't pulled is rejected; settings*.json, .mcp.json and .env files are never synced.
  • Stored as content-addressed files plus a small JSON manifest per version — no database, works the same self-hosted and on Stift Cloud.

Custom agents

any tool that keeps session state in files

Define it in ~/.config/stift/agents.json (override the path with STIFT_AGENTS):

~/.config/stift/agents.json
[
  { "name": "windsurf", "sessions": "~/.windsurf/runs/*" },
  { "name": "roo",      "sessions": "~/.roo/{md5}/tasks/*" },
  { "name": "notes",    "sessions": ".ai-notes/history.md" }
]

Two fields per agent:

  • name — lowercase letters, digits, dashes; must not clash with a built-in. Usable everywhere an agent name is (stift push --agent roo).
  • sessions — a glob pattern saying where sessions live. Each match becomes one session: a matched file is a single-file session, a matched directory is a session containing everything under it. The session id is derived from the matched name (run-7, history, …).

Pattern rules

  • ~/… patterns are home-based; anything else resolves against the project directory (like aider's in-project history files).
  • Many agents encode the project path into a directory name. Placeholders cover the common encodings and make project filtering work exactly like built-ins: {sha256} (Gemini-style hash), {md5} (Cursor-style), {munged} (Claude-style -work-app), {basename} (last path element).
  • A home-based pattern without a placeholder is treated as machine-global: detected on every push, stored without a project association.
  • Invalid entries are skipped with a warning; nothing outside your home (or project) directory is ever archived, even if a pattern tries.

Tokens & environment

one token per machine — revoke any one of them
sh
$ stift token create laptop        # prints the secret once
$ stift token create ci --admin    # admin = may manage tokens
$ stift token list
$ stift token revoke <id>
VariableUsed byMeaning
STIFT_SERVER, STIFT_TOKENclientoverride saved login (handy for CI)
STIFT_CONFIGclientconfig file path (default ~/.config/stift/config.json)
STIFT_SYNC_INTERVALdaemonbackground sync interval (default 30s)
STIFT_HOSTclient/daemonoverride this machine's host label (default OS hostname)
STIFT_SKILLS_STATEclientskills sync state (default ~/.config/stift/state.json)
STIFT_STATEdaemonsync-state cache path (default ~/.cache/stift/sync-state.json)
STIFT_LISTEN, STIFT_DATAserverlisten address / data directory
STIFT_ADMIN_TOKENserverregister a fixed admin token at startup

HTTP API

all /v1 endpoints require Authorization: Bearer <token>
Method & pathDescription
POST /v1/sessionsupload (multipart: meta JSON field, then archive tar.gz)
GET /v1/sessions?agent=&project=&host=&q=list, newest first
GET /v1/sessions/{id}metadata (id prefixes accepted)
GET /v1/sessions/{id}/archivedownload tar.gz
DELETE /v1/sessions/{id}delete
POST /v1/blobs/check · PUT/GET /v1/blobs/{sha}content-addressed files for skills
GET /v1/bundles?scope=&agent=&project=&name=list skill units (current versions)
PUT/GET/DELETE /v1/bundles/{scope}/{agent}/{name}one unit; ?version=, ?history=1, ?force=1; 409 when stale
GET /v1/whoamitoken name + role
GET/POST/DELETE /v1/tokenstoken management (admin only)
GET /healthzliveness (no auth)

Security notes

read before exposing anything
  • Tokens are stored hashed (SHA-256) on the server; secrets are shown once.
  • Run behind TLS — a reverse proxy (Caddy, nginx, Traefik) or your tunnel of choice. The server itself speaks plain HTTP.
  • Tar extraction rejects absolute paths and .. traversal.
Treat the data directory like source code. Session archives contain full conversation history, which often includes source code and may include secrets your agent saw. Treat the data directory and tokens accordingly.