cd /news/ai-tools/claude-code-wrapper-with-session-nam… · home topics ai-tools article
[ARTICLE · art-18401] src=gist.github.com pub= topic=ai-tools verified=true sentiment=· neutral

Claude Code wrapper with session naming and auto-summarization

A developer created a Bash wrapper for Claude Code that adds session naming, listing, and management capabilities to the AI coding tool. The wrapper, named `cc`, supports named sessions, session resumption by name or index, and includes an opt-in Gemini-based auto-summarization feature for previous sessions when listing. The tool also implements safer destructive operations by using `trash` instead of `rm` and requires explicit confirmation with session count display for clearing all sessions.

read20 min publishedMay 16, 2026

| #!/usr/bin/env bash | | | # cc - Claude Code wrapper with session naming | | | # Adapted from https://gist.github.com/zinknovo/cf21268e90419e9c0e93a9bd448591a4 | | | # | | | # Local changes: | | | # - Config dir defaults to ~/.claude-2 (override via $CLAUDE_CONFIG_DIR) | | | # - Destructive ops use trash instead of rm (per CLAUDE.md global rules) | | | # - clear requires typing "yes" + shows session count | |

| # - Gemini auto-summary is opt-in via CC_AUTO_SUMMARY=1 | |
| # - Removed _check_version (depended on external check-cli-compat) | |

| # | | | # Usage: | | | # cc Start a new Claude Code session | | | # cc n <name> Start a new session with a name | | | # cc ls List sessions (current project) | | | # cc lsa List sessions across all projects | | | # cc r <name|index> Resume a session by name or index | | | # cc rn <index> <name> Rename a session | | | # cc rm <name|index> Delete (trash) a session | | | # cc clear Delete (trash) all sessions | | | # | | | # Env: | | | # CLAUDE_CONFIG_DIR Default: $HOME/.claude-2 | |

| # CC_AUTO_SUMMARY=1 Enable Gemini-based prev-session auto-summary on `ls` | |
| # CLAUDE_CMD Underlying CLI command (default: claude) | |

| # CMD_NAME Display name in help text (default: basename $0) | | | set -euo pipefail | |

| CLAUDE_CMD="${CLAUDE_CMD:-$HOME/.local/bin/claude}" | |
| CMD_NAME="${CMD_NAME:-$(basename "$0")}" | |
| CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude-2}" | |

| export CLAUDE_CONFIG_DIR="$CLAUDE_DIR" | | | # Mirror the user's claude alias: write a profile marker so other tools | |

| # can detect which profile (claude-1 / claude-2) is active in this tty. | |
| _write_profile_marker() { | |
| local profile="${CLAUDE_DIR##*/.}" # ~/.claude-2 -> claude-2 | |

| local tty_id | |

| tty_id=$({ tty 2>/dev/null || true; } | tr '/' '_') | |
| [ -n "$tty_id" ] && [ "$tty_id" != "not a tty" ] && echo "$profile" > "/tmp/.claude_profile_$tty_id" 2>/dev/null || true | |

| } | | | _write_profile_marker | | | NAMES_DIR="$CLAUDE_DIR/session-names" | | | PROJECTS_DIR="$CLAUDE_DIR/projects" | | | SUMMARIES_DIR="$CLAUDE_DIR/session-summaries" | | | mkdir -p "$NAMES_DIR" "$SUMMARIES_DIR" | | | # Encode a path to Claude Code's project directory name | |

| # /Users/Z1nk -> -Users-Z1nk | |
| encode_project_path() { | |
| echo "$1" | sed -e 's|/|-|g' -e 's| |-|g' | |

| } | | | current_project_id() { | | | encode_project_path "$PWD" | | | } | |

| names_file() { | |
| local project_id="${1:-default}" | |

| echo "$NAMES_DIR/$project_id.json" | | | } | | | get_name() { | | | local project_id="$1" session_id="$2" | | | local nf | |

| nf=$(names_file "$project_id") | |
| [ -f "$nf" ] && python3 -c " | |

| import json, sys | |

| d = json.load(open(sys.argv[1])) | |
| print(d.get(sys.argv[2], '')) | |

| " "$nf" "$session_id" 2>/dev/null || echo "" | | | } | |

| set_name() { | |
| local project_id="$1" session_id="$2" name="$3" | |

| local nf | | | nf=$(names_file "$project_id") | | | python3 -c " | | | import json, os, sys | |

| nf, sid, name = sys.argv[1], sys.argv[2], sys.argv[3] | |
| d = {} | |
| if os.path.exists(nf): | |
| d = json.load(open(nf)) | |
| d[sid] = name | |
| json.dump(d, open(nf, 'w'), indent=2, ensure_ascii=False) | |

| " "$nf" "$session_id" "$name" | | | } | | | rm_name() { | | | local project_id="$1" session_id="$2" | | | local nf | |

| nf=$(names_file "$project_id") | |
| [ -f "$nf" ] && python3 -c " | |

| import json, os, sys | |

| nf, sid = sys.argv[1], sys.argv[2] | |
| d = json.load(open(nf)) | |
| d.pop(sid, None) | |
| json.dump(d, open(nf, 'w'), indent=2, ensure_ascii=False) | |

| " "$nf" "$session_id" 2>/dev/null | | | } | | | # Opt-in: generate Gemini summary for previous session in background | |

| generate_summary_for_latest() { | |
| [ "${CC_AUTO_SUMMARY:-0}" = "1" ] || return 0 | |
| command -v gemini >/dev/null 2>&1 || return 0 | |

| python3 - "$PWD" "$PROJECTS_DIR" "$SUMMARIES_DIR" <<'PYEOF' & | | | import json, os, sys, glob, subprocess | |

| cwd, projects_dir, summaries_dir = sys.argv[1], sys.argv[2], sys.argv[3] | |
| project_id = cwd.replace("/", "-").replace(" ", "-") | |
| project_dir = os.path.join(projects_dir, project_id) | |
| if not os.path.isdir(project_dir): | |
| sys.exit(0) | |
| sessions = [] | |
| for f in glob.glob(os.path.join(project_dir, "*.jsonl")): | |
| sid = os.path.basename(f).replace(".jsonl", "") | |
| sessions.append((sid, f, os.path.getmtime(f))) | |
| sessions.sort(key=lambda x: x[2], reverse=True) | |
| for sid, fpath, _ in sessions[1:2]: | |
| summary_file = os.path.join(summaries_dir, f"{sid}.txt") | |
| if os.path.exists(summary_file): | |

| continue | | | user_msgs = [] | | | try: | | | with open(fpath) as fh: | | | for line in fh: | | | line = line.strip() | | | if not line: | | | continue | | | try: | |

| record = json.loads(line) | |
| if record.get("type") != "user": | |

| continue | |

| msg = record.get("message", {}) | |
| content = msg.get("content", "") | |

| text = "" | |

| if isinstance(content, str): | |
| text = content.strip() | |
| elif isinstance(content, list): | |

| for c in content: | |

| if isinstance(c, dict) and c.get("type") == "text": | |
| t = c.get("text", "").strip() | |

| if t: | | | text = t | | | break | | | if text: | | | user_msgs.append(text[:500]) | | | except Exception: | | | continue | | | except Exception: | | | continue | | | if len(user_msgs) < 2: | | | continue | | | selected = user_msgs[:5] + user_msgs[-5:] if len(user_msgs) > 10 else user_msgs | | | prompt_text = "Summarize this conversation in one short sentence (max 50 chars, same language as the conversation). Just output the summary, nothing else.\n\n" | |

| for i, m in enumerate(selected, 1): | |
| prompt_text += f"User message {i}: {m}\n\n" | |

| try: | |

| result = subprocess.run( | |
| ["gemini", "-p", prompt_text], | |
| capture_output=True, text=True, timeout=15 | |

| ) | |

| summary = result.stdout.strip().replace("\n", " ")[:80] | |
| if summary and "error" not in summary.lower()[:20]: | |
| with open(summary_file, "w") as sf: | |
| sf.write(summary) | |

| except Exception: | | | pass | | | PYEOF | | | } | | | cmd_list() { | | | local all_projects=false | | | [ "${1:-}" = "-a" ] && all_projects=true | | | python3 - "$all_projects" "$PWD" "$PROJECTS_DIR" "$NAMES_DIR" "$SUMMARIES_DIR" <<'PYEOF' | | | import json, os, sys, glob | | | from datetime import datetime, timezone | |

| all_projects = sys.argv[1] == "true" | |
| cwd = sys.argv[2] | |
| projects_dir = sys.argv[3] | |
| names_dir = sys.argv[4] | |
| summaries_dir = sys.argv[5] | |
| home = os.path.expanduser("~") | |

| desktop_sessions_dir = os.path.join(home, "Library", "Application Support", "Claude", "claude-code-sessions") | |

| def encode_path(p): | |
| return p.replace("/", "-").replace(" ", "-") | |
| def decode_path(encoded): | |
| if not encoded.startswith("-"): | |

| return encoded | | | parts = encoded[1:].split("-") | | | result = "" | | | i = 0 | |

| while i < len(parts): | |
| for j in range(len(parts), i, -1): | |
| candidate = result + "/" + "-".join(parts[i:j]) | |
| if os.path.isdir(candidate) or j == i + 1: | |

| result = candidate | | | i = j | | | break | |

| return result if result else "/" + encoded[1:].replace("-", "/") | |
| def get_display_path(encoded): | |
| decoded = decode_path(encoded) | |
| if decoded.startswith(home): | |
| return "~" + decoded[len(home):] | |
| elif decoded == "/": | |

| return "/" | | | return decoded | |

| def load_desktop_titles(): | |
| titles = {} | |
| if not os.path.isdir(desktop_sessions_dir): | |

| return titles | | | for f in glob.glob(os.path.join(desktop_sessions_dir, "", "", "*.json")): | | | try: | |

| d = json.load(open(f)) | |
| cli_sid = d.get("cliSessionId", "") | |
| title = d.get("title", "") | |

| if cli_sid and title: | | | titles[cli_sid] = title | | | except Exception: | | | continue | | | return titles | |

| desktop_titles = load_desktop_titles() | |
| current_project = encode_path(cwd) | |
| sessions = [] | |
| for project_dir in sorted(glob.glob(os.path.join(projects_dir, "*"))): | |
| if not os.path.isdir(project_dir): | |

| continue | | | project_id = os.path.basename(project_dir) | | | if not all_projects and project_id != current_project: | | | continue | |

| nf = os.path.join(names_dir, f"{project_id}.json") | |
| names = {} | |
| if os.path.exists(nf): | |

| try: | | | names = json.load(open(nf)) | | | except Exception: | | | pass | |

| display_path = get_display_path(project_id) | |
| for f in glob.glob(os.path.join(project_dir, "*.jsonl")): | |
| fname = os.path.basename(f) | |
| sid = fname.replace(".jsonl", "") | |

| msg_count = 0 | | | last_user_msg = "" | | | last_time = "" | | | try: | | | with open(f) as fh: | | | for line in fh: | | | line = line.strip() | | | if not line: | | | continue | | | try: | |

| record = json.loads(line) | |
| rtype = record.get("type", "") | |
| ts = record.get("timestamp", "") | |

| if ts: | | | last_time = ts | | | if rtype == "user": | | | msg_count += 1 | |

| msg = record.get("message", {}) | |
| content = msg.get("content", "") | |

| text = "" | |

| if isinstance(content, str): | |
| text = content.replace("\n", " ").strip() | |
| elif isinstance(content, list): | |

| for c in content: | |

| if isinstance(c, dict) and c.get("type") == "text": | |
| text = c.get("text", "").replace("\n", " ").strip() | |

| if text: | | | break | | | if text: | |

| last_user_msg = text[:50] | |
| elif rtype == "assistant": | |

| msg_count += 1 | | | except Exception: | | | continue | | | except Exception: | | | continue | | | if msg_count == 0: | | | continue | | | custom_name = desktop_titles.get(sid, "") or names.get(sid, "") | | | summary = "" | |

| sf = os.path.join(summaries_dir, f"{sid}.txt") | |
| if os.path.exists(sf): | |

| try: | | | summary = open(sf).read().strip()[:80] | | | except Exception: | | | pass | | | time_str = "" | | | if last_time: | | | try: | |

| if isinstance(last_time, (int, float)): | |
| dt = datetime.fromtimestamp(last_time / 1000, tz=timezone.utc) | |

| else: | |

| dt = datetime.fromisoformat(str(last_time).replace("Z", "+00:00")) | |
| now = datetime.now(timezone.utc) | |
| diff = now - dt | |
| if diff.days > 0: | |
| time_str = f"{diff.days}d ago" | |
| elif diff.seconds > 3600: | |
| time_str = f"{diff.seconds // 3600}h ago" | |

| else: | | | time_str = f"{diff.seconds // 60}m ago" | | | except Exception: | |

| time_str = str(last_time)[:16] | |
| sessions.append({ | |

| "project": project_id, | | | "project_path": display_path, | | | "sid": sid, | | | "name": custom_name, | | | "summary": summary, | | | "last_msg": last_user_msg, | | | "time": time_str, | | | "sort_time": last_time, | | | "msgs": msg_count, | |

| }) | |
| sessions.sort(key=lambda s: s["sort_time"] if s["sort_time"] else "", reverse=True) | |

| if not sessions: | |

| print(" No sessions found.") | |
| sys.exit(0) | |

| current_proj = "" | |

| for i, s in enumerate(sessions, 1): | |
| if all_projects and s["project"] != current_proj: | |
| current_proj = s["project"] | |
| print(f" \033[0;33m{s['project_path']}\033[0m") | |
| proj = f"\033[0;33m{s['project_path']}\033[0m" if not all_projects else "" | |
| preview = s["summary"] or s["last_msg"] or "(empty)" | |
| line = f" {i:>3}. {s['time']:>8} {s['msgs']:>3} msgs" | |

| if not all_projects: | |

| line += f" {proj}" | |
| if s["name"]: | |
| line += f" \033[1;36m{s['name']}\033[0m" | |
| if s["summary"]: | |
| line += f" ({s['summary'][:40]})" | |

| else: | |

| line += f" {preview}" | |
| print(line) | |

| PYEOF | | | } | | | resolve_session() { | | | local query="$1" | | | local all_flag="${2:-}" | | | python3 - "$query" "$PWD" "$all_flag" "$PROJECTS_DIR" "$NAMES_DIR" <<'PYEOF' | head -1 | | | import json, os, sys, glob | |

| query = sys.argv[1] | |
| cwd = sys.argv[2] | |
| all_flag = sys.argv[3] | |
| projects_dir = sys.argv[4] | |
| names_dir = sys.argv[5] | |
| def encode_path(p): | |
| return p.replace("/", "-").replace(" ", "-") | |
| current_project = encode_path(cwd) | |
| sessions = [] | |
| for project_dir in sorted(glob.glob(os.path.join(projects_dir, "*"))): | |
| if not os.path.isdir(project_dir): | |

| continue | |

| project_id = os.path.basename(project_dir) | |
| if all_flag != "-a" and project_id != current_project: | |

| continue | |

| nf = os.path.join(names_dir, f"{project_id}.json") | |
| names = {} | |
| if os.path.exists(nf): | |

| try: | | | names = json.load(open(nf)) | | | except Exception: | | | pass | |

| for f in glob.glob(os.path.join(project_dir, "*.jsonl")): | |
| sid = os.path.basename(f).replace(".jsonl", "") | |

| msg_count = 0 | | | last_time = "" | | | try: | | | with open(f) as fh: | | | for line in fh: | | | line = line.strip() | | | if not line: | | | continue | | | try: | |

| r = json.loads(line) | |
| if r.get("type") in ("user", "assistant"): | |

| msg_count += 1 | | | ts = r.get("timestamp", "") | | | if ts: | | | last_time = ts | | | except Exception: | | | continue | | | except Exception: | | | continue | | | if msg_count == 0: | | | continue | |

| custom_name = names.get(sid, "") | |
| sessions.append({"sid": sid, "name": custom_name, "project": project_id, "sort_time": last_time}) | |
| sessions.sort(key=lambda s: s["sort_time"] if s["sort_time"] else "", reverse=True) | |

| try: | |

| idx = int(query) - 1 | |
| if 0 <= idx < len(sessions): | |
| print(sessions[idx]["sid"]) | |
| sys.exit(0) | |

| except ValueError: | | | pass | | | # Match by session ID (full UUID or unambiguous prefix, min 4 chars) | |

| if len(query) >= 4 and all(c in "0123456789abcdef-" for c in query.lower()): | |
| q = query.lower() | |
| exact = [s for s in sessions if s["sid"].lower() == q] | |

| if exact: | |

| print(exact[0]["sid"]) | |
| sys.exit(0) | |
| prefix = [s for s in sessions if s["sid"].lower().startswith(q)] | |
| if len(prefix) == 1: | |
| print(prefix[0]["sid"]) | |
| sys.exit(0) | |
| if len(prefix) > 1: | |
| print(f"ERROR: session id prefix '{query}' is ambiguous ({len(prefix)} matches)", file=sys.stderr) | |
| sys.exit(1) | |

| for s in sessions: | |

| if s["name"] == query: | |
| print(s["sid"]) | |
| sys.exit(0) | |

| for s in sessions: | |

| if s["name"] and query.lower() in s["name"].lower(): | |
| print(s["sid"]) | |
| sys.exit(0) | |
| print(f"ERROR: No session found matching '{query}'", file=sys.stderr) | |
| sys.exit(1) | |

| PYEOF | | | } | | | resolve_project_for_session() { | | | local session_id="$1" | | | python3 -c " | | | import os, glob, sys | |

| sid = sys.argv[1] | |
| projects_dir = sys.argv[2] | |
| for f in glob.glob(os.path.join(projects_dir, '*', sid + '.jsonl')): | |
| print(os.path.basename(os.path.dirname(f))) | |

| break | | | " "$session_id" "$PROJECTS_DIR" 2>/dev/null | | | } | | | find_newest_session_after() { | | | local ts="$1" | | | python3 - "$ts" "$PWD" "$PROJECTS_DIR" <<'PYEOF' | head -1 | | | import os, sys, glob | |

| ts = int(sys.argv[1]) | |
| cwd = sys.argv[2] | |
| projects_dir = sys.argv[3] | |
| project_id = cwd.replace("/", "-").replace(" ", "-") | |
| project_dir = os.path.join(projects_dir, project_id) | |

| newest = None | | | newest_time = ts | |

| if os.path.isdir(project_dir): | |
| for f in glob.glob(os.path.join(project_dir, "*.jsonl")): | |

| try: | |

| mtime = int(os.path.getmtime(f) * 1000) | |
| if mtime > ts and mtime > newest_time: | |
| newest = os.path.basename(f).replace(".jsonl", "") | |

| newest_time = mtime | | | except Exception: | | | continue | | | if newest: | | | print(newest) | | | PYEOF | | | } | | | launch_and_name() { | | | local session_name="$1" | | | shift | | | local ts | | | ts=$(python3 -c "import time; print(int(time.time() * 1000))") | | | "$CLAUDE_CMD" "$@" | | | local sid | |

| sid=$(find_newest_session_after "$ts") | |
| if [ -n "$sid" ] && [ -n "$session_name" ]; then | |

| local project_id | |

| project_id=$(resolve_project_for_session "$sid") | |
| if [ -n "$project_id" ]; then | |

| set_name "$project_id" "$sid" "$session_name" | | | echo "Session saved as: $session_name" | | | fi | | | fi | | | } | | | case "${1:-}" in | | | ls) | | | generate_summary_for_latest | | | cmd_list "" | | | ;; | | | lsa) | | | generate_summary_for_latest | | | cmd_list "-a" | | | ;; | | | r|resume) | |

| [ -z "${2:-}" ] && echo "Usage: $CMD_NAME r <name|index>" && exit 1 | |
| # Numeric index resolves in current-project scope (matches `cc ls`); | |
| # names/IDs resolve cross-project (matches `cc lsa`). | |
| if [[ "$2" =~ ^[0-9]+$ ]]; then | |
| sid=$(resolve_session "$2" "") | |

| else | | | sid=$(resolve_session "$2" "-a") | | | fi | | | if [ -n "$sid" ]; then | | | exec "$CLAUDE_CMD" --resume "$sid" | | | fi | | | ;; | | | rn|rename) | | | { [ -z "${2:-}" ] || [ -z "${3:-}" ]; } && echo "Usage: $CMD_NAME rn <index> <name>" && exit 1 | | | target="$2" | | | shift 2 | |

| sid=$(resolve_session "$target" "-a") | |
| if [ -n "$sid" ]; then | |
| project_id=$(resolve_project_for_session "$sid") | |

| set_name "$project_id" "$sid" "$" | | | echo "Named session as: $" | | | fi | | | ;; | | | rm|delete) | |

| [ -z "${2:-}" ] && echo "Usage: $CMD_NAME rm <name|index|session-id>" && exit 1 | |
| sid=$(resolve_session "$2" "-a" 2>/dev/null) | |

| # Fallback: treat arg as a raw session id and locate the file on disk | |

| if [ -z "$sid" ] && [[ "$2" =~ ^[0-9a-fA-F-]{4,}$ ]]; then | |
| match=$(find "$PROJECTS_DIR" -maxdepth 2 -name "$2*.jsonl" 2>/dev/null) | |
| count=$(printf "%s\n" "$match" | grep -c .) | |
| if [ "$count" = "1" ]; then | |
| sid=$(basename "$match" .jsonl) | |
| elif [ "$count" -gt 1 ]; then | |

| echo "ERROR: session id '$2' matches $count files" >&2 | | | exit 1 | | | fi | | | fi | |

| if [ -n "$sid" ]; then | |
| project_id=$(resolve_project_for_session "$sid") | |

| session_file="$PROJECTS_DIR/$project_id/$sid.jsonl" | | | session_dir="$PROJECTS_DIR/$project_id/$sid" | |

| [ -f "$session_file" ] && trash "$session_file" | |
| [ -d "$session_dir" ] && trash "$session_dir" | |
| [ -n "$project_id" ] && rm_name "$project_id" "$sid" | |

| echo "Trashed session: $sid" | | | else | | | echo "ERROR: No session found matching '$2'" >&2 | | | exit 1 | | | fi | | | ;; | | | clear) | | | count=$(find "$PROJECTS_DIR" -name "*.jsonl" 2>/dev/null | wc -l | tr -d ' ') | | | echo "This will move $count session file(s) and all name maps to the Trash." | | | echo -n "Type 'yes' to confirm: " | | | read -r confirm | | | if [ "$confirm" = "yes" ]; then | | | # Collect files into an array so trash gets explicit paths | |

| while IFS= read -r f; do | |
| [ -n "$f" ] && trash "$f" | |
| done < <(find "$PROJECTS_DIR" -name "*.jsonl" 2>/dev/null) | |

| for nf in "$NAMES_DIR"/.json; do | | | [ -f "$nf" ] && trash "$nf" | | | done | | | echo "All sessions trashed." | | | else | | | echo "Cancelled." | | | fi | | | ;; | | | n|new) | | | [ -z "${2:-}" ] && echo "Usage: $CMD_NAME n <name>" && exit 1 | | | shift | | | launch_and_name "$" | |

| ;; | |
| help|-h|--help) | |

| cat <<EOF | | | $CMD_NAME - Claude Code wrapper with session naming | | | Usage: | | | $CMD_NAME n <name> Start a new session with a name | | | $CMD_NAME ls List sessions (current project) | | | $CMD_NAME lsa List sessions across all projects | | | $CMD_NAME r <name|index> Resume session (numeric index = current project, like 'ls'; | | | name/UUID = cross-project, like 'lsa') | | | $CMD_NAME rn <index> <name> Rename a session | | | $CMD_NAME rm <name|index|id> Trash a session (by name, list index, or session UUID/prefix) | | | $CMD_NAME clear Trash all sessions (with confirmation) | | | $CMD_NAME help Show this help | | | Note: sessions are scoped per profile via CLAUDE_CONFIG_DIR. Two profiles | | | (e.g. ~/.claude and ~/.claude-2) have independent session lists — switching | | | profile changes what 'ls' and 'r <index>' see. | | | Env: | | | CLAUDE_CONFIG_DIR=$CLAUDE_DIR | | | CC_AUTO_SUMMARY=${CC_AUTO_SUMMARY:-0} (set to 1 to enable Gemini summaries) | | | EOF | | | ;; | | | "") | | | # No args: behave like a plain claude launcher (matches former alias) | | | exec "$CLAUDE_CMD" | | | ;; | | | *) | | | echo "Unknown command: $1 (try '$CMD_NAME help')" && exit 1 | | | ;; | | | esac |

── more in #ai-tools 4 stories · sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/claude-code-wrapper-…] indexed:0 read:20min 2026-05-16 ·