Skip to content

Commit e8a5890

Browse files
smypmsaclaude
andcommitted
feat: pumpfun-cli — Solana pump.fun trading CLI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
0 parents  commit e8a5890

112 files changed

Lines changed: 22079 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/hooks/bash-guard.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env python3
2+
"""Pre-bash guard: block dangerous git operations, warn on transactions.
3+
4+
BLOCKS (exit 1): git add of docs/*.md (gitignored files).
5+
ADVISORY (exit 0): transaction commands, mainnet tests, sensitive file deletion.
6+
"""
7+
8+
import json
9+
import re
10+
import sys
11+
12+
data = json.load(sys.stdin)
13+
command = data.get("tool_input", {}).get("command", "")
14+
15+
# --- Advisory warnings (exit 0 — never block) ---
16+
17+
# Transaction-sending CLI commands
18+
TX_COMMANDS = re.compile(
19+
r"\bpumpfun\b.*\b("
20+
r"buy|sell|transfer|launch|migrate|"
21+
r"close-atas|claim-cashback|close-volume-acc|collect-creator-fee"
22+
r")\b"
23+
)
24+
25+
# Mainnet test scripts
26+
MAINNET_PATTERNS = re.compile(r"mainnet[_-]test|mainnet\.sh")
27+
28+
# Deletion of sensitive files
29+
SENSITIVE_DELETE = re.compile(
30+
r"\brm\b.*("
31+
r"\.env|wallet\.enc|\.pem|\.key|\.secret"
32+
r")"
33+
)
34+
35+
# Deletion of IDL directory
36+
IDL_DELETE = re.compile(r"\brm\b.*\bidl[/\s]")
37+
38+
# git add of docs/*.md files (gitignored — must never be staged)
39+
GIT_ADD_DOCS_MD = re.compile(r"\bgit\s+add\b.*\bdocs/.*\.md\b")
40+
41+
if GIT_ADD_DOCS_MD.search(command):
42+
print(
43+
"BLOCKED: docs/*.md files are in .gitignore and must not be staged. "
44+
"Remove docs/ paths from your git add command.",
45+
file=sys.stderr,
46+
)
47+
sys.exit(1)
48+
49+
elif TX_COMMANDS.search(command):
50+
print(
51+
"NOTE: This command sends a Solana transaction. On mainnet this costs real SOL.",
52+
file=sys.stderr,
53+
)
54+
55+
elif MAINNET_PATTERNS.search(command):
56+
print(
57+
"NOTE: This runs mainnet end-to-end tests (costs real SOL). "
58+
"Make sure the user has confirmed.",
59+
file=sys.stderr,
60+
)
61+
62+
elif SENSITIVE_DELETE.search(command):
63+
print(
64+
"NOTE: This deletes a sensitive file (.env, wallet, or credentials). "
65+
"Make sure the user intended this.",
66+
file=sys.stderr,
67+
)
68+
69+
elif IDL_DELETE.search(command):
70+
print(
71+
"NOTE: This deletes IDL files (source-of-truth from on-chain programs). "
72+
"These cannot be regenerated without fetching from chain.",
73+
file=sys.stderr,
74+
)
75+
76+
# Always allow — this is advisory only
77+
sys.exit(0)

.claude/hooks/guard.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/env python3
2+
"""Pre-edit guard: block writes to sensitive files."""
3+
4+
import json
5+
import os
6+
import sys
7+
8+
data = json.load(sys.stdin)
9+
file_path = data.get("tool_input", {}).get("file_path", "")
10+
proj = os.environ.get("CLAUDE_PROJECT_DIR", "")
11+
12+
# --- Protected patterns ---
13+
BLOCKED_EXACT = {".env", ".env.local", ".env.production"}
14+
BLOCKED_DIRS = {"idl/"}
15+
BLOCKED_SUFFIXES = (".pem", ".key", ".secret")
16+
BLOCKED_NAMES = {"wallet.enc"}
17+
18+
rel = os.path.relpath(file_path, proj) if proj else file_path
19+
name = os.path.basename(rel)
20+
21+
if rel in BLOCKED_EXACT:
22+
print(f"BLOCKED: {rel} is a secrets file — edit manually", file=sys.stderr)
23+
sys.exit(2)
24+
25+
for d in BLOCKED_DIRS:
26+
if rel.startswith(d):
27+
print(
28+
f"BLOCKED: {rel} is in {d} (source-of-truth from on-chain) — don't edit",
29+
file=sys.stderr,
30+
)
31+
sys.exit(2)
32+
33+
if rel.endswith(BLOCKED_SUFFIXES):
34+
print(f"BLOCKED: {rel} looks like a credential file", file=sys.stderr)
35+
sys.exit(2)
36+
37+
if name in BLOCKED_NAMES:
38+
print(f"BLOCKED: {name} is the encrypted wallet keystore — don't edit", file=sys.stderr)
39+
sys.exit(2)

.claude/hooks/lint.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env python3
2+
"""Post-edit lint hook: ruff format + check on changed Python files."""
3+
4+
import json
5+
import os
6+
import subprocess
7+
import sys
8+
9+
data = json.load(sys.stdin)
10+
file_path = data.get("tool_input", {}).get("file_path", "")
11+
12+
# Only lint Python files
13+
if not file_path.endswith(".py"):
14+
sys.exit(0)
15+
16+
# Only lint files inside our project
17+
proj = os.environ.get("CLAUDE_PROJECT_DIR", "")
18+
if proj and not file_path.startswith(proj):
19+
sys.exit(0)
20+
21+
exit_code = 0
22+
23+
# Format the single file (uses project's [tool.ruff] config via uv run)
24+
subprocess.run(
25+
["uv", "run", "ruff", "format", file_path],
26+
capture_output=True,
27+
cwd=proj or None,
28+
)
29+
30+
# Check the single file with auto-fix
31+
result = subprocess.run(
32+
["uv", "run", "ruff", "check", "--fix", file_path],
33+
capture_output=True,
34+
text=True,
35+
cwd=proj or None,
36+
)
37+
38+
if result.returncode != 0:
39+
output = result.stdout + result.stderr
40+
if output.strip():
41+
print(output, end="", file=sys.stderr)
42+
exit_code = 2
43+
44+
sys.exit(exit_code)

.claude/hooks/test-hooks.sh

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#!/usr/bin/env bash
2+
# Functional tests for Claude Code hooks.
3+
# Run from project root: ./.claude/hooks/test-hooks.sh
4+
5+
set -euo pipefail
6+
7+
PROJ="$(cd "$(dirname "$0")/../.." && pwd)"
8+
PASS=0
9+
FAIL=0
10+
11+
run_test() {
12+
local name="$1" hook="$2" input="$3" expect_exit="$4" expect_stderr="$5"
13+
14+
actual_stderr=$(echo "$input" | CLAUDE_PROJECT_DIR="$PROJ" uv run python "$PROJ/.claude/hooks/$hook" 2>&1 1>/dev/null || true)
15+
actual_exit=$(echo "$input" | CLAUDE_PROJECT_DIR="$PROJ" uv run python "$PROJ/.claude/hooks/$hook" >/dev/null 2>/dev/null; echo $?)
16+
17+
local ok=true
18+
19+
if [[ "$actual_exit" != "$expect_exit" ]]; then
20+
echo "FAIL: $name — expected exit $expect_exit, got $actual_exit"
21+
ok=false
22+
fi
23+
24+
if [[ -n "$expect_stderr" && "$actual_stderr" != *"$expect_stderr"* ]]; then
25+
echo "FAIL: $name — stderr missing '$expect_stderr'"
26+
echo " got: $actual_stderr"
27+
ok=false
28+
fi
29+
30+
if [[ -z "$expect_stderr" && -n "$actual_stderr" ]]; then
31+
echo "FAIL: $name — expected no stderr, got: $actual_stderr"
32+
ok=false
33+
fi
34+
35+
if $ok; then
36+
echo "PASS: $name"
37+
PASS=$((PASS + 1))
38+
else
39+
FAIL=$((FAIL + 1))
40+
fi
41+
}
42+
43+
echo "=== guard.py (PreToolUse Write|Edit) ==="
44+
45+
run_test "blocks .env" "guard.py" \
46+
'{"tool_input":{"file_path":"'"$PROJ"'/.env"}}' \
47+
"2" "BLOCKED: .env is a secrets file"
48+
49+
run_test "blocks .env.production" "guard.py" \
50+
'{"tool_input":{"file_path":"'"$PROJ"'/.env.production"}}' \
51+
"2" "BLOCKED: .env.production is a secrets file"
52+
53+
run_test "blocks idl/ files" "guard.py" \
54+
'{"tool_input":{"file_path":"'"$PROJ"'/idl/pump_fun_idl.json"}}' \
55+
"2" "source-of-truth from on-chain"
56+
57+
run_test "blocks wallet.enc" "guard.py" \
58+
'{"tool_input":{"file_path":"'"$PROJ"'/some/path/wallet.enc"}}' \
59+
"2" "encrypted wallet keystore"
60+
61+
run_test "blocks .pem files" "guard.py" \
62+
'{"tool_input":{"file_path":"'"$PROJ"'/certs/server.pem"}}' \
63+
"2" "credential file"
64+
65+
run_test "blocks .key files" "guard.py" \
66+
'{"tool_input":{"file_path":"'"$PROJ"'/certs/private.key"}}' \
67+
"2" "credential file"
68+
69+
run_test "allows normal Python files" "guard.py" \
70+
'{"tool_input":{"file_path":"'"$PROJ"'/src/pumpfun_cli/cli.py"}}' \
71+
"0" ""
72+
73+
run_test "allows .env.example" "guard.py" \
74+
'{"tool_input":{"file_path":"'"$PROJ"'/.env.example"}}' \
75+
"0" ""
76+
77+
run_test "allows README.md" "guard.py" \
78+
'{"tool_input":{"file_path":"'"$PROJ"'/README.md"}}' \
79+
"0" ""
80+
81+
echo ""
82+
echo "=== bash-guard.py (PreToolUse Bash) — advisory only ==="
83+
84+
run_test "warns on pumpfun buy (exit 0)" "bash-guard.py" \
85+
'{"tool_input":{"command":"uv run pumpfun buy ABC123 0.1"}}' \
86+
"0" "sends a Solana transaction"
87+
88+
run_test "warns on pumpfun sell (exit 0)" "bash-guard.py" \
89+
'{"tool_input":{"command":"pumpfun sell ABC123 100%"}}' \
90+
"0" "sends a Solana transaction"
91+
92+
run_test "warns on pumpfun transfer (exit 0)" "bash-guard.py" \
93+
'{"tool_input":{"command":"uv run pumpfun transfer 1.0 dest"}}' \
94+
"0" "sends a Solana transaction"
95+
96+
run_test "warns on pumpfun launch (exit 0)" "bash-guard.py" \
97+
'{"tool_input":{"command":"pumpfun launch --name TEST"}}' \
98+
"0" "sends a Solana transaction"
99+
100+
run_test "warns on pumpfun migrate (exit 0)" "bash-guard.py" \
101+
'{"tool_input":{"command":"uv run pumpfun migrate ABC123"}}' \
102+
"0" "sends a Solana transaction"
103+
104+
run_test "warns on mainnet-test.sh (exit 0)" "bash-guard.py" \
105+
'{"tool_input":{"command":"./scripts/mainnet-test.sh"}}' \
106+
"0" "mainnet end-to-end tests"
107+
108+
run_test "warns on rm .env (exit 0)" "bash-guard.py" \
109+
'{"tool_input":{"command":"rm .env"}}' \
110+
"0" "deletes a sensitive file"
111+
112+
run_test "warns on rm wallet.enc (exit 0)" "bash-guard.py" \
113+
'{"tool_input":{"command":"rm -f ~/.config/pumpfun-cli/wallet.enc"}}' \
114+
"0" "deletes a sensitive file"
115+
116+
run_test "warns on rm -rf idl/ (exit 0)" "bash-guard.py" \
117+
'{"tool_input":{"command":"rm -rf idl/"}}' \
118+
"0" "deletes IDL files"
119+
120+
run_test "silent on pytest (exit 0)" "bash-guard.py" \
121+
'{"tool_input":{"command":"uv run pytest tests/ -q"}}' \
122+
"0" ""
123+
124+
run_test "silent on git status (exit 0)" "bash-guard.py" \
125+
'{"tool_input":{"command":"git status"}}' \
126+
"0" ""
127+
128+
run_test "silent on uv sync (exit 0)" "bash-guard.py" \
129+
'{"tool_input":{"command":"uv sync --dev"}}' \
130+
"0" ""
131+
132+
echo ""
133+
echo "=== lint.py (PostToolUse Write|Edit) ==="
134+
135+
run_test "runs on Python files (exit 0)" "lint.py" \
136+
'{"tool_input":{"file_path":"'"$PROJ"'/src/pumpfun_cli/cli.py"}}' \
137+
"0" ""
138+
139+
run_test "skips non-Python files (exit 0)" "lint.py" \
140+
'{"tool_input":{"file_path":"'"$PROJ"'/README.md"}}' \
141+
"0" ""
142+
143+
run_test "skips files outside project (exit 0)" "lint.py" \
144+
'{"tool_input":{"file_path":"/tmp/random.py"}}' \
145+
"0" ""
146+
147+
echo ""
148+
echo "================================"
149+
echo "PASS: $PASS FAIL: $FAIL"
150+
if [[ $FAIL -gt 0 ]]; then
151+
echo "SOME TESTS FAILED"
152+
exit 1
153+
else
154+
echo "ALL TESTS PASSED"
155+
fi

.claude/settings.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"hooks": {
3+
"PreToolUse": [
4+
{
5+
"matcher": "Write|Edit",
6+
"hooks": [
7+
{
8+
"type": "command",
9+
"command": "uv run python \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard.py\"",
10+
"timeout": 10
11+
}
12+
]
13+
},
14+
{
15+
"matcher": "Bash",
16+
"hooks": [
17+
{
18+
"type": "command",
19+
"command": "uv run python \"$CLAUDE_PROJECT_DIR/.claude/hooks/bash-guard.py\"",
20+
"timeout": 10
21+
}
22+
]
23+
}
24+
],
25+
"PostToolUse": [
26+
{
27+
"matcher": "Write|Edit",
28+
"hooks": [
29+
{
30+
"type": "command",
31+
"command": "uv run python \"$CLAUDE_PROJECT_DIR/.claude/hooks/lint.py\"",
32+
"timeout": 30
33+
}
34+
]
35+
}
36+
]
37+
}
38+
}

0 commit comments

Comments
 (0)