Skip to content
Open
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
1 change: 1 addition & 0 deletions plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)<br>**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<br>**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

Expand Down
9 changes: 9 additions & 0 deletions plugins/stash/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
116 changes: 116 additions & 0 deletions plugins/stash/README.md
Original file line number Diff line number Diff line change
@@ -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 <message>`

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 <id>`

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)
51 changes: 51 additions & 0 deletions plugins/stash/commands/stash-apply.md
Original file line number Diff line number Diff line change
@@ -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 <id>` 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.
43 changes: 43 additions & 0 deletions plugins/stash/commands/stash-list.md
Original file line number Diff line number Diff line change
@@ -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.
45 changes: 45 additions & 0 deletions plugins/stash/commands/stash-pop.md
Original file line number Diff line number Diff line change
@@ -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.
64 changes: 64 additions & 0 deletions plugins/stash/commands/stash.md
Original file line number Diff line number Diff line change
@@ -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 <message>` 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": <next_id>, "message": "<message>", "timestamp": "<ISO8601>"}`
4. Print: `Message stashed. #<id>`

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.