Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# Changelog

## Unreleased
## 0.4.0: agents use the CLI, and every decision is a card

**`init --undo` keeps what you added since `init`.** Undo used to copy the whole saved file back over a shared config (`~/.claude.json`, `~/.codex/config.toml`, an agent's `mcp.json`, an `AGENTS.md`), so another MCP server added after `init --mcp` was lost. It now takes tokenstash's entry or section out of the file as it is and puts back what the file held under that name before; a file `init` created that holds nothing else is removed. Backups are named by a digest of the full path, so `/a_b/c/AGENTS.md` and `/a/b_c/AGENTS.md` no longer share one. Every file `init` writes is written beside its target and renamed into place, keeping its permissions, so a full disk or a crash leaves the old file rather than half of a new one.

**Open the inbox from your other computers over Tailscale.** On a machine you reach over SSH or Tailscale, links pointed at its 127.0.0.1, which does not open on your computer, and desktop notifications went nowhere. `tokenstash remote tailscale` (an agent may run it) makes the inbox also listen on this machine's Tailscale address, and every link and notification point there. A request from another device signed in to the same Tailscale account counts as you, as `tailscale whois` reports it: the agent's link opens the card with your full session, so you approve from your laptop without a terminal. Nothing else on the tailnet gets an answer, this machine's own Tailscale address gets what loopback gets, and posts still need the session cookie and the matching hidden field. `tokenstash remote off` turns it off at once, and a command that changes `config.toml` reads it again under a lock and changes only its own settings, so an `init` that finishes later does not turn it back on. `doctor` shows the setting and checks that the inbox itself answers on the Tailscale address. When nothing is set and the agent runs in an SSH login or on a Linux machine with no desktop, each pending result's `next` says the user may be on another computer and how to reach the inbox from there.
**Agents use the CLI, through one skill.** `init` installs a tokenstash skill for each agent it finds: `~/.claude/skills/tokenstash` for Claude Code, `~/.agents/skills/tokenstash` for Codex and Gemini CLI, `~/.cursor/skills/tokenstash` for Cursor. The skill documents the CLI for agents: `SKILL.md` covers requesting keys, what each result means and what the user sees on each card, and `reference.md` and `troubleshooting.md` beside it list every command, flag, exit code, JSON field and setting, and how to debug tokenstash itself. An agent sees only the skill's one-line description until code needs a key. `init` no longer registers the MCP server by default (`init --mcp` does, `--no-mcp` takes it out, and the choice is kept as `mcp` in `config.toml`), and it no longer writes AGENTS.md sections: it takes out the section earlier versions put in `~/.codex/AGENTS.md` and any `init --project` wrote, the Codex `/prompts:tokenstash` prompt and the Gemini CLI command. `--project` is accepted with a notice; `--print-snippet` is an error. Explicit mode is the same skill, marked for the person to invoke (`/tokenstash`; `$tokenstash` in Codex, through `agents/openai.yaml`). A plain `init` from an agent's shell now installs the skill too; choosing the mode, `--mcp` and `--undo` stay a person's.

**Nothing you decide needs a terminal.** When you ask your agent to replace or forget a key, use another identity in a project, or change how agents reach tokenstash, it runs the command and you confirm on a card: `forget`, `bind` and `init --mode`, `--mcp`, `--no-mcp` and `--undo` from an agent file a confirm card and change nothing until you confirm it from your own inbox link. `rotate` from an agent files a Replace card without marking the key stale, so the old key keeps working until you paste the new one. `need --force` from an agent asks once more for a key you declined, once per key and identity in each project until the "no" expires, and the card says it is a second ask. `list`, `audit` and `check` show an agent the keys and events of its own directory instead of refusing. A card opened from the agent's link that needs your own session (an approval, a Replace card, a confirm card) has a "Send the link to my desktop" button, which shows the desktop notification again; the inbox pages no longer tell you to run `tokenstash open`. `TOKENSTASH_AGENT=unknown` no longer passes the terminal check: any value of it now marks an agent.

**Agents use the CLI, through one skill.** `init` installs a tokenstash skill for each agent it finds: `~/.claude/skills/tokenstash` for Claude Code, `~/.agents/skills/tokenstash` for Codex and Gemini CLI, `~/.cursor/skills/tokenstash` for Cursor. The skill documents the CLI for agents: `SKILL.md` covers requesting keys, what each result means and what the user sees on each card, and `reference.md` and `troubleshooting.md` beside it list every command, flag, exit code, JSON field and setting, and how to debug tokenstash itself. An agent sees only the skill's one-line description until code needs a key. `init` no longer registers the MCP server by default (`init --mcp` does, `--no-mcp` takes it out, and the choice is kept as `mcp` in `config.toml`), and it no longer writes AGENTS.md sections: it takes out the section earlier versions put in `~/.codex/AGENTS.md` and any `init --project` wrote, the Codex `/prompts:tokenstash` prompt and the Gemini CLI command. `--project` is accepted with a notice; `--print-snippet` is an error. Explicit mode is the same skill, marked for the person to invoke (`/tokenstash`; `$tokenstash` in Codex, through `agents/openai.yaml`). A plain `init` from an agent's shell now installs the skill too; choosing the mode, `--mcp` and `--undo` stay a person's.
**Open the inbox from your other computers over Tailscale.** On a machine you reach over SSH or Tailscale, links pointed at its 127.0.0.1, which does not open on your computer, and desktop notifications went nowhere. `tokenstash remote tailscale` (an agent may run it) makes the inbox also listen on this machine's Tailscale address, and every link and notification point there. A request from another device signed in to the same Tailscale account counts as you, as `tailscale whois` reports it: the agent's link opens the card with your full session, so you approve from your laptop without a terminal. Nothing else on the tailnet gets an answer, this machine's own Tailscale address gets what loopback gets, and posts still need the session cookie and the matching hidden field. `tokenstash remote off` turns it off at once, and a command that changes `config.toml` reads it again under a lock and changes only its own settings, so an `init` that finishes later does not turn it back on. `doctor` shows the setting and checks that the inbox itself answers on the Tailscale address. When nothing is set and the agent runs in an SSH login or on a Linux machine with no desktop, each pending result's `next` says the user may be on another computer and how to reach the inbox from there.

**A replaced key kept coming back on Linux machines without a Secret Service.** On a headless box or an SSH login, tokenstash stores keys in the kernel keyring. It used keyring-rs's keyutils store, which keeps a copy of each key in every login session's keyring and reads that copy first. A key pasted from one session (an SSH shell, the inbox) was shadowed in another (the agent's) by the copy that session had read earlier, and that read put the old copy back into the shared persistent keyring. The old key was written over the new one in the env file at the agent's next `need`. tokenstash now keeps one key object per name, linked into the user keyring and the persistent keyring, never reads a session copy ahead of them, and updates a key in place, so an older tokenstash still running in another session reads the new value as well. Keys stored by older versions are found where they are. Writes take a lock, so two processes storing the same new key cannot leave the two keyrings holding different values. A key no longer disappears after the persistent keyring's three days without use while any process of yours is running; a reboot still clears the kernel keyring, and `doctor` and `init` say so, and say when the kernel has no persistent keyring at all (keys then last only while you have a process running).

**`need` says what to do next.** Every result from `tokenstash need`, in text and with `--json`, carries the same `next` instruction the MCP tool returns: where the key is, which kind of card is waiting and what the user does with it, or what to do after a denial. A link in that text is never followed by punctuation, so it copies cleanly.

**A replaced key kept coming back on Linux machines without a Secret Service.** On a headless box or an SSH login, tokenstash stores keys in the kernel keyring. It used keyring-rs's keyutils store, which keeps a copy of each key in every login session's keyring and reads that copy first. A key pasted from one session (an SSH shell, the inbox) was shadowed in another (the agent's) by the copy that session had read earlier, and that read put the old copy back into the shared persistent keyring. The old key was written over the new one in the env file at the agent's next `need`. tokenstash now keeps one key object per name, linked into the user keyring and the persistent keyring, never reads a session copy ahead of them, and updates a key in place, so an older tokenstash still running in another session reads the new value as well. Keys stored by older versions are found where they are. Writes take a lock, so two processes storing the same new key cannot leave the two keyrings holding different values. A key no longer disappears after the persistent keyring's three days without use while any process of yours is running; a reboot still clears the kernel keyring, and `doctor` and `init` say so, and say when the kernel has no persistent keyring at all (keys then last only while you have a process running).
**`init --undo` keeps what you added since `init`.** Undo used to copy the whole saved file back over a shared config (`~/.claude.json`, `~/.codex/config.toml`, an agent's `mcp.json`, an `AGENTS.md`), so another MCP server added after `init --mcp` was lost. It now takes tokenstash's entry or section out of the file as it is and puts back what the file held under that name before; a file `init` created that holds nothing else is removed. Backups are named by a digest of the full path, so `/a_b/c/AGENTS.md` and `/a/b_c/AGENTS.md` no longer share one. Every file `init` writes is written beside its target and renamed into place, keeping its permissions, so a full disk or a crash leaves the old file rather than half of a new one.

**A provider's verdict applies only to the value it checked.** `report-bad`, `check` and verify-on-use send a stored key to its provider and then mark it stale or verified. A key the human stored while that request was out used to receive the old key's verdict. A 401 marked the new key stale and filed another Replace card, and an Ok cleared a stale flag the new key had earned. The verdict is now recorded only if the stash still holds the value that was sent, and the comparison and the update run under the index write lock that a store and `import` hold across their stash writes. `check` reports a key it could not reach without waiting for that lock.

Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ resolver = "2"
members = ["crates/core", "crates/cli"]

[workspace.package]
version = "0.3.0"
version = "0.4.0"
edition = "2021"
license = "MIT"
repository = "https://github.com/krishhgg/tokenstash"
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ name = "tokenstash"
path = "src/main.rs"

[dependencies]
tokenstash-core = { path = "../core", version = "0.3.0" }
tokenstash-core = { path = "../core", version = "0.4.0" }
anyhow.workspace = true
serde.workspace = true
serde_json.workspace = true
Expand Down
10 changes: 5 additions & 5 deletions npm/tokenstash/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tokenstash",
"version": "0.3.0",
"version": "0.4.0",
"description": "Credential broker for coding agents: paste a key once, approve each directory, keep secrets out of status output.",
"bin": { "tokenstash": "bin/tokenstash.js" },
"files": ["bin", "README.md", "LICENSE"],
Expand All @@ -10,9 +10,9 @@
"keywords": ["api-keys", "secrets", "agents", "claude-code", "codex", "cursor", "mcp"],
"engines": { "node": ">=18" },
"optionalDependencies": {
"tokenstash-darwin-arm64": "0.3.0",
"tokenstash-darwin-x64": "0.3.0",
"tokenstash-linux-arm64": "0.3.0",
"tokenstash-linux-x64": "0.3.0"
"tokenstash-darwin-arm64": "0.4.0",
"tokenstash-darwin-x64": "0.4.0",
"tokenstash-linux-arm64": "0.4.0",
"tokenstash-linux-x64": "0.4.0"
}
}
Loading