Security
In short
- Full prompts, file contents and command output stay on the developer's machine, in files only that user can read. Nothing leaves the machine until it is connected to a server.
-
The default upload contains no text from prompts, commands or files, and no
absolute paths. Previews and line hashes are separate switches, off by default,
and
sessionly exportprints exactly what would be sent. - The capture hook only observes. It cannot block or change an agent's session, and it never opens the sign-in files of Cursor or Grok.
- Access to a server is by token. Tokens are stored as SHA-256 hashes; a member reads only their own sessions, and the token the GitHub Action uses can report pull requests and nothing else.
- Nothing phones home: no telemetry, no update checks, and licenses are verified offline.
- Found a problem? Email kristiyan@sessionly.dev. The code is public at github.com/KIvanow/sessionly.
Sessionly is operated by Kristiyan Ivanov, an individual. This page describes what the software does to protect the sessions it records, as it is built today. It is written from the code, which you can read yourself; where this page and the code disagree, the code is right and I would like to hear about it. It is not a certification: Sessionly has no SOC 2 or ISO 27001 audit.
Sessionly is self-hosted today: hosting, TLS and backups of a server are yours to arrange. A hosted service is planned and not open yet; how it is hosted and backed up will be described here before it opens.
On the developer's machine
What is kept, and who can read it
The sessionly program keeps everything under ~/.sessionly:
- The raw capture: every hook event as the agent sent it, including full prompts, file contents and command output. Files are created with mode 0600 and directories with 0700, so only your user can read them. The raw capture is never uploaded.
- The ledger: one record per session in a local SQLite file, also 0600. A record holds metadata and short previews, not content: for a prompt, its length, SHA-256 and first 200 characters; for a tool call, its tool, status, duration, file path, a hash of its input, the size of its output and a one-line summary.
- The configuration, including the server's address and your token, in a file readable only by you. The token is never printed.
There is no retention limit yet: the raw capture grows until you delete it. Deleting
~/.sessionly removes everything Sessionly has kept on that machine.
The capture hook
-
It only observes.
sessionly installregisters 20 events that report what happened. Events that change the agent's behaviour when hooked (creating and removing worktrees, switching models) are left out on purpose. - It cannot get in the way. The hook appends one line under a file lock, never prints, and always exits 0, whatever its input or arguments, so a capture failure cannot block or alter a session. If another capture holds the file for more than 1.5 seconds, it writes anyway instead of waiting.
- It is careful with your settings. Install adds, and uninstall removes, only hook entries whose command is exactly Sessionly's own. Other hooks and settings, file permissions and a symlinked location are kept; the file as it was before the first change is saved beside it and never overwritten; a settings file that is not valid JSON is left untouched.
Other agents' files
Sessions from Cursor and Grok are read from the transcript and session files those agents keep. Their sign-in files are never opened.
What leaves the machine
Nothing, until you run sessionly connect. The program has one network
connection: the upload to the server you connected. It sends no telemetry and does not
check for updates. Once connected, before a record is uploaded:
-
Paths are made relative: to the repository for files inside it,
otherwise to the home directory (
~/…). No absolute path under your home directory is sent. -
The content level is applied. The default,
metadata, sends counts, sizes, timings, tool names, statuses, relative file paths, models, tokens, cost, rewrite figures and the git user and remote. It sends no text from prompts, commands, search patterns, URLs, errors or titles, and no content hashes.previews, which you switch on yourself, adds the 200-character previews and the hashes. - Line hashes are a separate switch, off by default. When on, the upload includes, per changed file and model, the first 8 bytes of the SHA-256 of each line the agent added, without the whitespace at its ends. Blank lines and lines with no letter or digit are left out.
sessionly export <session> prints the exact payload for any session,
connected or not, and sessionly schema upload prints its JSON Schema.
In transit
The program refuses plain http:// for any server that is not on the same
machine, so an upload to a remote server always goes over HTTPS. The token travels in
the Authorization header.
On the server
-
Tokens (
sly_…, 24 random bytes) are shown once when created. The server stores their SHA-256 hash and looks them up by it. A token can be revoked at any time, and when it was last used is recorded. There are no passwords. -
Roles. A
membertoken uploads sessions and reads only the sessions filed under its owner. Anadmintoken reads everyone's and manages tokens. Acitoken, for the GitHub Action, can report pull requests and check that it is accepted; every other request is refused. - Ownership. An upload is filed under the owner of the token that sent it, whatever name the record carries, and a session uploaded by one person cannot be replaced by another.
- Validation. Every upload is checked against the published schema and refused if it does not match; a record marked as local-only is refused; a body over 32 MB is refused.
- The first admin token comes from the server's environment, for use before any token exists. It is compared in constant time and never stored.
- Logs. The server does not write a log of requests. Unexpected errors are written to its standard error.
- The dashboard keeps the token in the browser's local storage and sends it as a header. It sets no cookies and loads nothing from other sites.
Self-hosting defaults
Started directly, the server listens on 127.0.0.1 only. The Docker Compose
file publishes the server's port and keeps Postgres inside the Compose network, with a
fixed database password that is not reachable from outside it. Put the server behind
HTTPS before anyone outside the machine uses it; the program will not upload to it
otherwise.
Pull requests
The GitHub Action and the program both send hashes of lines, never the lines. The server compares them to tell which lines of a pull request an agent added. No source code leaves GitHub or anyone's machine for this.
-
The Action is plain JavaScript with no dependencies. It needs
pull-requests: readand nothing else unless you ask it to comment. -
It authenticates with a
citoken kept as a repository secret. If that token leaks, it can submit pull request reports; it cannot read sessions, reports or tokens. - GitHub does not give secrets to workflows run from forks, so pull requests from forks are not reported.
- The commit email addresses the Action sends are used to pick the sessions to compare with and are not stored.
Licenses
A Business or Enterprise license is a JSON file with an Ed25519 signature over its contents. The server verifies it against a public key built into the code, when it is installed and each time it is loaded, and accepts it only if it names the install's own org. Nothing is sent anywhere to check it. The private key is not in the repository.
This website
The website is static HTML, one stylesheet and two typefaces, all served from the site
itself. It runs no scripts, sets no cookies and has no analytics. It is served with a
Content Security Policy of default-src 'none' that allows only its own
stylesheet, fonts and images, and sends X-Content-Type-Options, X-Frame-Options and
Referrer-Policy headers. It is hosted by Vercel, which keeps request logs.
What Sessionly cannot protect you from
- A preview can contain a secret. The first 200 characters of a prompt or a shell command are whatever was typed. They are kept in the local ledger, and uploaded if previews are on. Sessionly does not scan for secrets.
- Paths and hashes still say something. A relative path names files in your repository. A line hash is 64 bits of a SHA-256: it does not give the line back, but a short or common line can be guessed and checked against it. Treat line hashes as you would the names of your files.
- A token is the whole key. Anyone holding an admin token can read every session in the org. Tokens do not expire on their own; revoke them when people leave.
- Identity is a label. The name and email in a record come from git configuration and are not verified. The server trusts the token, not the name.
- Anyone with your user account can read the capture. File permissions keep other users out, not other programs running as you, and the files are not encrypted.
Reporting a vulnerability
If you believe you have found a security problem in Sessionly or this website, please email kristiyan@sessionly.dev with enough detail to reproduce it. I read that address myself. Please give me a reasonable window to fix the issue before you publish it, and do not access, change or delete other people's data while demonstrating it. There is no bug bounty.
The whole codebase, including the capture program, the server, the dashboard and the
GitHub Action, is public at
github.com/KIvanow/sessionly, with
everything outside ee/ under Apache-2.0. Run it yourself and read what it
does.