diff --git a/plugins/README.md b/plugins/README.md
index cf4a21ecc5..0ff14930b9 100644
--- a/plugins/README.md
+++ b/plugins/README.md
@@ -25,6 +25,7 @@ Learn more in the [official plugins documentation](https://docs.claude.com/en/do
| [pr-review-toolkit](./pr-review-toolkit/) | Comprehensive PR review agents specializing in comments, tests, error handling, type design, code quality, and code simplification | **Command:** `/pr-review-toolkit:review-pr` - Run with optional review aspects (comments, tests, errors, types, code, simplify, all)
**Agents:** `comment-analyzer`, `pr-test-analyzer`, `silent-failure-hunter`, `type-design-analyzer`, `code-reviewer`, `code-simplifier` |
| [ralph-wiggum](./ralph-wiggum/) | Interactive self-referential AI loops for iterative development. Claude works on the same task repeatedly until completion | **Commands:** `/ralph-loop`, `/cancel-ralph` - Start/stop autonomous iteration loops
**Hook:** Stop - Intercepts exit attempts to continue iteration |
| [security-guidance](./security-guidance/) | Security reminder hook that warns about potential security issues when editing files | **Hook:** PreToolUse - Monitors 9 security patterns including command injection, XSS, eval usage, dangerous HTML, pickle deserialization, and os.system calls |
+| [stash](./stash/) | Git-stash-like message storage with push, pop, apply, and list operations persisted to disk | **Commands:** `/stash`, `/stash-pop`, `/stash-apply`, `/stash-list` - Multi-slot message stash stack |
## Installation
diff --git a/plugins/stash/.claude-plugin/plugin.json b/plugins/stash/.claude-plugin/plugin.json
new file mode 100644
index 0000000000..a868bcf3a4
--- /dev/null
+++ b/plugins/stash/.claude-plugin/plugin.json
@@ -0,0 +1,9 @@
+{
+ "name": "stash",
+ "description": "Git-stash-like message storage with push, pop, apply, and list operations. Persists messages to disk in a JSONL stack so they survive crashes and session changes.",
+ "version": "1.0.0",
+ "author": {
+ "name": "Leonardo Cardoso",
+ "url": "https://github.com/LeonardoCardoso"
+ }
+}
diff --git a/plugins/stash/README.md b/plugins/stash/README.md
new file mode 100644
index 0000000000..1cec0921d0
--- /dev/null
+++ b/plugins/stash/README.md
@@ -0,0 +1,116 @@
+# Stash Plugin
+
+Git-stash-like message storage for Claude Code. Push, pop, apply, and list messages persisted to disk so they survive crashes and session changes.
+
+Addresses [#26615](https://github.com/anthropics/claude-code/issues/26615) — the built-in Ctrl+S stash is single-slot and not persisted to disk.
+
+## Commands
+
+### `/stash `
+
+Save a message to the stash stack.
+
+```bash
+/stash Refactor the auth module to use JWT tokens instead of session cookies. Start with the middleware in src/auth/...
+```
+
+Output:
+```
+Message stashed.
+#0 Refactor the auth module to use JWT tokens instead of session cookies. Start with the middleware in src/auth/...
+```
+
+### `/stash-pop`
+
+Apply and remove the **last** stashed message.
+
+```bash
+/stash-pop
+```
+
+Output:
+```
+#2 2026-02-27T14:30:00Z
+
+Refactor the auth module to use JWT tokens...
+```
+
+### `/stash-apply `
+
+Apply and remove a **specific** stashed message by ID.
+
+```bash
+/stash-apply 0
+```
+
+Output:
+```
+#0 2026-02-27T12:00:00Z
+
+Add error handling to the payment service...
+```
+
+### `/stash-list`
+
+List all stashed messages (truncated to 80 characters).
+
+```bash
+/stash-list
+```
+
+Output:
+```
+#0 2026-02-27T12:00:00Z Add error handling to the payment service when Stripe returns a 429...
+#1 2026-02-27T13:15:00Z Write integration tests for the new search endpoint covering edge ca...
+#2 2026-02-27T14:30:00Z Refactor the auth module to use JWT tokens instead of session cookie...
+
+3 stashed message(s)
+```
+
+## Storage
+
+Messages are stored in `~/.claude/stash.jsonl` — one JSON object per line:
+
+```json
+{"id": 0, "message": "Add error handling to the payment service...", "timestamp": "2026-02-27T12:00:00Z"}
+{"id": 1, "message": "Write integration tests for the new search endpoint...", "timestamp": "2026-02-27T13:15:00Z"}
+```
+
+The file is human-readable and can be edited manually if needed.
+
+## Why This Exists
+
+The built-in Ctrl+S stash has several limitations:
+- **Single slot** — stashing again overwrites the previous stash
+- **Not persisted to disk** — lost on crashes, errors, or session changes
+- **No history** — no way to browse or select from multiple stashes
+
+This plugin provides a proper stack with push/pop/apply semantics, persisted to a JSONL file that survives any disruption.
+
+## Requirements
+
+- Python 3 (pre-installed on macOS and most Linux distributions)
+
+## Installation
+
+Install via the Claude Code plugin command:
+
+```bash
+claude plugin add --path ./stash
+```
+
+Or add to your project/user settings:
+
+```json
+{
+ "plugins": ["./path/to/stash"]
+}
+```
+
+## Version
+
+1.0.0
+
+## Author
+
+Leonardo Cardoso — [github.com/LeonardoCardoso](https://github.com/LeonardoCardoso)
diff --git a/plugins/stash/commands/stash-apply.md b/plugins/stash/commands/stash-apply.md
new file mode 100644
index 0000000000..593ac282bc
--- /dev/null
+++ b/plugins/stash/commands/stash-apply.md
@@ -0,0 +1,51 @@
+---
+allowed-tools: Bash(python3:*)
+description: Apply a stashed message by ID (and remove it)
+---
+
+## Your task
+
+Apply and remove a specific message from `~/.claude/stash.jsonl` by its ID. The ID is `$ARGUMENTS`.
+
+If `$ARGUMENTS` is empty, reply with: `Usage: /stash-apply ` and stop.
+
+Run a single bash command:
+
+```bash
+python3 -c "
+import json, os, sys
+
+stash_path = os.path.expanduser('~/.claude/stash.jsonl')
+target_id = int(sys.argv[1])
+
+if not os.path.exists(stash_path):
+ print('Stash is empty.')
+ sys.exit(0)
+
+entries = []
+found = None
+with open(stash_path, 'r') as f:
+ for line in f:
+ line = line.strip()
+ if line:
+ entry = json.loads(line)
+ if entry['id'] == target_id:
+ found = entry
+ else:
+ entries.append(entry)
+
+if not found:
+ print(f'Stash #{target_id} not found.')
+ sys.exit(0)
+
+with open(stash_path, 'w') as f:
+ for e in entries:
+ f.write(json.dumps(e) + '\n')
+
+print(f'#{found[\"id\"]} {found[\"timestamp\"]}')
+print()
+print(found['message'])
+" "$ARGUMENTS"
+```
+
+Print only the output of that command. Do not send any other text.
diff --git a/plugins/stash/commands/stash-list.md b/plugins/stash/commands/stash-list.md
new file mode 100644
index 0000000000..b8ea360120
--- /dev/null
+++ b/plugins/stash/commands/stash-list.md
@@ -0,0 +1,43 @@
+---
+allowed-tools: Bash(python3:*)
+description: List all stashed messages
+---
+
+## Your task
+
+List all messages in `~/.claude/stash.jsonl`.
+
+Run a single bash command:
+
+```bash
+python3 -c "
+import json, os
+
+stash_path = os.path.expanduser('~/.claude/stash.jsonl')
+
+if not os.path.exists(stash_path):
+ print('Stash is empty.')
+ raise SystemExit(0)
+
+entries = []
+with open(stash_path, 'r') as f:
+ for line in f:
+ line = line.strip()
+ if line:
+ entries.append(json.loads(line))
+
+if not entries:
+ print('Stash is empty.')
+ raise SystemExit(0)
+
+for e in entries:
+ msg = e['message']
+ preview = (msg[:77] + '...') if len(msg) > 80 else msg
+ preview = preview.replace('\n', ' ')
+ print(f'#{e[\"id\"]} {e[\"timestamp\"]} {preview}')
+
+print(f'\n{len(entries)} stashed message(s)')
+"
+```
+
+Print only the output of that command. Do not send any other text.
diff --git a/plugins/stash/commands/stash-pop.md b/plugins/stash/commands/stash-pop.md
new file mode 100644
index 0000000000..ac4fd5deb8
--- /dev/null
+++ b/plugins/stash/commands/stash-pop.md
@@ -0,0 +1,45 @@
+---
+allowed-tools: Bash(python3:*)
+description: Pop the last stashed message (apply and remove)
+---
+
+## Your task
+
+Pop the last message from `~/.claude/stash.jsonl` — print it and remove it from the file.
+
+Run a single bash command:
+
+```bash
+python3 -c "
+import json, os, sys
+
+stash_path = os.path.expanduser('~/.claude/stash.jsonl')
+
+if not os.path.exists(stash_path):
+ print('Stash is empty.')
+ sys.exit(0)
+
+entries = []
+with open(stash_path, 'r') as f:
+ for line in f:
+ line = line.strip()
+ if line:
+ entries.append(json.loads(line))
+
+if not entries:
+ print('Stash is empty.')
+ sys.exit(0)
+
+last = entries.pop()
+
+with open(stash_path, 'w') as f:
+ for e in entries:
+ f.write(json.dumps(e) + '\n')
+
+print(f'#{last[\"id\"]} {last[\"timestamp\"]}')
+print()
+print(last['message'])
+"
+```
+
+Print only the output of that command. Do not send any other text.
diff --git a/plugins/stash/commands/stash.md b/plugins/stash/commands/stash.md
new file mode 100644
index 0000000000..d5298d26ed
--- /dev/null
+++ b/plugins/stash/commands/stash.md
@@ -0,0 +1,64 @@
+---
+allowed-tools: Bash(mkdir:*), Bash(python3:*)
+description: Stash a message for later use
+---
+
+## Your task
+
+Stash the user's message to `~/.claude/stash.jsonl`. The message is everything in `$ARGUMENTS`.
+
+If `$ARGUMENTS` is empty, reply with: `Usage: /stash ` and stop.
+
+Run a single bash command to do the following atomically:
+
+1. Create `~/.claude/` if it doesn't exist
+2. Read the current max ID from the stash file (or start at 0 if the file doesn't exist)
+3. Append a new JSON line with `{"id": , "message": "", "timestamp": ""}`
+4. Print: `Message stashed. #`
+
+Use `python3` for this:
+
+```bash
+python3 -c "
+import json, os, sys
+from datetime import datetime, timezone
+
+stash_path = os.path.expanduser('~/.claude/stash.jsonl')
+os.makedirs(os.path.dirname(stash_path), exist_ok=True)
+
+max_id = -1
+if os.path.exists(stash_path):
+ with open(stash_path, 'r') as f:
+ for line in f:
+ line = line.strip()
+ if line:
+ entry = json.loads(line)
+ max_id = max(max_id, entry['id'])
+
+new_id = max_id + 1
+entry = {
+ 'id': new_id,
+ 'message': sys.argv[1],
+ 'timestamp': datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
+}
+
+with open(stash_path, 'a') as f:
+ f.write(json.dumps(entry) + '\n')
+
+msg = sys.argv[1]
+if len(msg) > 100:
+ # Find the nearest word boundary around 100 chars
+ cut = msg.rfind(' ', 0, 110)
+ if cut <= 50:
+ cut = 100
+ preview = msg[:cut] + '...'
+else:
+ preview = msg
+preview = preview.replace('\n', ' ')
+
+print('Message stashed.')
+print(f'#{new_id} {preview}')
+" "$ARGUMENTS"
+```
+
+Print only the output of that command. Do not send any other text.