Skip to content

Instantly share code, notes, and snippets.

@possibilities
Created February 2, 2026 01:35
Show Gist options
  • Select an option

  • Save possibilities/c09ef6017a610aae666a9d1a0efb995c to your computer and use it in GitHub Desktop.

Select an option

Save possibilities/c09ef6017a610aae666a9d1a0efb995c to your computer and use it in GitHub Desktop.

Plan: /compose:show-session-id using SessionStart hook

Summary

Use a SessionStart hook to capture the session_id and store it in the existing pane JSON state file. The slash command then reads this value directly via a bash script.

How it works

  1. SessionStart hook - When Claude starts/resumes, the hook receives session_id in its input JSON
  2. Store in pane state - The hook updates /tmp/arthack-claude/panes/{uuid}/{session}-{pane}.json to include session_id
  3. Slash command - Calls a script that reads the session_id from the pane JSON

Files to Modify

1. Remove existing hook (cleanup from previous attempt)

  • Delete plugins/compose/hooks/show_session_id.py
  • Remove its entry from plugins/compose/hooks/hooks.json

2. Create SessionStart hook

File: plugins/compose/hooks/session_start.py

#!/usr/bin/env -S uv run --script
import json
import os
import sys
from pathlib import Path

def main():
    data = json.load(sys.stdin)

    session_id = data.get("session_id")
    if not session_id:
        sys.exit(0)

    # Get tmux info from environment
    server_uuid = os.environ.get("ARTHACK_SESSION_UUID")
    tmux_pane = os.environ.get("TMUX_PANE")
    tmux_session = os.environ.get("TMUX", "").split(",")[0] if os.environ.get("TMUX") else None

    if not all([server_uuid, tmux_pane]):
        sys.exit(0)

    # Find pane JSON file
    pane_dir = Path(f"/tmp/arthack-claude/panes/{server_uuid}")
    if not pane_dir.exists():
        sys.exit(0)

    # Find file matching pattern *-{pane}.json
    for pane_file in pane_dir.glob(f"*-{tmux_pane}.json"):
        try:
            pane_data = json.loads(pane_file.read_text())
            pane_data["session_id"] = session_id
            pane_file.write_text(json.dumps(pane_data, indent=2))
        except (json.JSONDecodeError, OSError):
            pass
        break

    # Output empty response (no context injection needed)
    print(json.dumps({"hookSpecificOutput": {"hookEventName": "SessionStart"}}))

if __name__ == "__main__":
    main()

3. Update hooks.json

Add SessionStart hook entry:

"SessionStart": [
  {
    "hooks": [
      {
        "type": "command",
        "command": "${CLAUDE_PLUGIN_ROOT}/hooks/session_start.py"
      }
    ]
  }
]

4. Create reader script

File: plugins/compose/scripts/get-session-id.sh

#!/usr/bin/env bash
set -euo pipefail

server_uuid="${ARTHACK_SESSION_UUID:-}"
tmux_pane="${TMUX_PANE:-}"

if [[ -z "$server_uuid" || -z "$tmux_pane" ]]; then
    echo "error: not running in arthack-claude tmux environment"
    exit 1
fi

pane_dir="/tmp/arthack-claude/panes/${server_uuid}"
if [[ ! -d "$pane_dir" ]]; then
    echo "error: pane directory not found"
    exit 1
fi

# Find matching pane file
for f in "$pane_dir"/*-"${tmux_pane}".json; do
    if [[ -f "$f" ]]; then
        session_id=$(jq -r '.session_id // empty' "$f")
        if [[ -n "$session_id" ]]; then
            echo "$session_id"
            exit 0
        fi
    fi
done

echo "error: session_id not found in pane state"
exit 1

5. Update slash command

File: plugins/compose/commands/show-session-id.md

---
description: Show session ID
allowed-tools: Bash(get-session-id.sh:*)
---

!`${CLAUDE_PLUGIN_ROOT}/scripts/get-session-id.sh`

Verification

  1. Start a new Claude session with arthack-claude
  2. Run /compose:show-session-id
  3. Should output a UUID like 328c11e4-0c9d-4921-871a-fd561727dcc9
  4. Verify the session_id was added to pane JSON: cat /tmp/arthack-claude/panes/$ARTHACK_SESSION_UUID/*-$TMUX_PANE.json | jq .session_id
"""
viewctl - Gist-like file viewer for sharing code snippets.
Run `viewctl --help` for usage.
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
# Add scripts/bin to path for shared imports
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from common import CleanHelpFormatter
def _lazy_import(module_name: str, func_name: str = "run"):
"""Return a lazy-loading wrapper that imports on first call."""
def wrapper(args: argparse.Namespace) -> int:
import importlib
module = importlib.import_module(module_name)
func = getattr(module, func_name)
return func(args)
return wrapper
def build_parser() -> argparse.ArgumentParser:
"""Build the argument parser."""
parser = argparse.ArgumentParser(
add_help=False,
description="Gist-like file viewer for sharing code snippets.",
formatter_class=CleanHelpFormatter,
)
parser.add_argument("-h", "--help", action="help", help=argparse.SUPPRESS)
subparsers = parser.add_subparsers(dest="command", metavar="<command>")
# create-view subcommand
create_parser = subparsers.add_parser(
"create-view",
help="Create a view from one or more files",
)
create_parser.add_argument(
"files",
nargs="+",
help="Files to include in the view",
)
create_parser.set_defaults(func=_lazy_import("commands.viewctl.run_create_view"))
# serve-views subcommand
serve_parser = subparsers.add_parser(
"serve-views",
help="Start the web server to display views",
)
serve_parser.add_argument(
"--port",
type=int,
default=8080,
help="Port to serve on (default: 8080)",
)
serve_parser.add_argument(
"--watch",
action="store_true",
help="Enable hot reload on file changes",
)
serve_parser.set_defaults(func=_lazy_import("commands.viewctl.run_serve_views"))
return parser
def main() -> int:
"""Main entry point."""
parser = build_parser()
args = parser.parse_args()
if hasattr(args, "func"):
return args.func(args)
else:
parser.print_help()
return 0
if __name__ == "__main__":
sys.exit(main())
"""
Helper functions for viewctl.
Provides view directory management, manifest handling, and ID generation.
"""
from __future__ import annotations
import secrets
import string
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
import yaml
# Views directory
VIEWS_DIR = Path.home() / ".config" / "viewctl" / "views"
def ensure_views_dir() -> None:
"""Ensure views directory exists."""
VIEWS_DIR.mkdir(parents=True, exist_ok=True)
def generate_view_id() -> str:
"""Generate a random 8-character alphanumeric ID."""
alphabet = string.ascii_lowercase + string.digits
return "".join(secrets.choice(alphabet) for _ in range(8))
def get_view_dir(view_id: str) -> Path:
"""Get the directory path for a view."""
return VIEWS_DIR / view_id
def load_manifest(view_id: str) -> dict[str, Any] | None:
"""Load manifest for a view."""
manifest_path = get_view_dir(view_id) / "_manifest.yaml"
if not manifest_path.exists():
return None
return yaml.safe_load(manifest_path.read_text())
def save_manifest(view_id: str, manifest: dict[str, Any]) -> None:
"""Save manifest for a view."""
view_dir = get_view_dir(view_id)
view_dir.mkdir(parents=True, exist_ok=True)
manifest_path = view_dir / "_manifest.yaml"
manifest_path.write_text(
yaml.dump(manifest, default_flow_style=False, sort_keys=False)
)
def list_views() -> list[dict[str, Any]]:
"""List all views with their manifests."""
if not VIEWS_DIR.exists():
return []
views = []
for view_dir in sorted(
VIEWS_DIR.iterdir(), key=lambda p: p.stat().st_mtime, reverse=True
):
if view_dir.is_dir():
manifest = load_manifest(view_dir.name)
if manifest:
views.append(
{
"id": view_dir.name,
**manifest,
}
)
return views
def get_view_with_contents(view_id: str) -> dict[str, Any] | None:
"""Get a view with file contents."""
manifest = load_manifest(view_id)
if not manifest:
return None
view_dir = get_view_dir(view_id)
files = []
for file_info in manifest.get("files", []):
file_path = view_dir / file_info["name"]
if file_path.exists():
files.append(
{
"name": file_info["name"],
"display_path": file_info["display_path"],
"content": file_path.read_text(),
}
)
return {
"id": view_id,
"created": manifest.get("created"),
"files": files,
}
def now_iso() -> str:
"""Return current UTC time in ISO format."""
return datetime.now(tz=UTC).isoformat()
#!/usr/bin/env bunx bun
import { readdir, readFile } from "fs/promises";
import { join } from "path";
import { homedir } from "os";
import { parse as parseYaml } from "yaml";
const VIEWS_DIR = join(homedir(), ".config", "viewctl", "views");
// Parse command line arguments
const args = process.argv.slice(2);
let port = 8080;
for (let i = 0; i < args.length; i++) {
if (args[i] === "--port" && args[i + 1]) {
port = parseInt(args[i + 1], 10);
}
}
async function loadManifest(viewId) {
try {
const manifestPath = join(VIEWS_DIR, viewId, "_manifest.yaml");
const content = await readFile(manifestPath, "utf-8");
return parseYaml(content);
} catch {
return null;
}
}
async function listViews() {
try {
const entries = await readdir(VIEWS_DIR, { withFileTypes: true });
const views = [];
for (const entry of entries) {
if (entry.isDirectory()) {
const manifest = await loadManifest(entry.name);
if (manifest) {
views.push({ id: entry.name, ...manifest });
}
}
}
// Sort by created date, newest first
views.sort((a, b) => new Date(b.created) - new Date(a.created));
return views;
} catch {
return [];
}
}
async function getViewWithContents(viewId) {
const manifest = await loadManifest(viewId);
if (!manifest) return null;
const viewDir = join(VIEWS_DIR, viewId);
const files = [];
for (const fileInfo of manifest.files || []) {
try {
const content = await readFile(join(viewDir, fileInfo.name), "utf-8");
files.push({
name: fileInfo.name,
display_path: fileInfo.display_path,
language: fileInfo.language || null,
content,
});
} catch {
// Skip files that can't be read
}
}
return {
id: viewId,
created: manifest.created,
files,
};
}
const HTML_TEMPLATE = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>viewctl</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/geist@1/dist/fonts/geist-mono/style.min.css">
<script src="https://cdn.tailwindcss.com"></script>
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18",
"react-dom/client": "https://esm.sh/react-dom@18/client",
"shiki": "https://esm.sh/shiki@1",
"marked": "https://esm.sh/marked@12"
}
}
</script>
<style>
:root {
--background: oklch(0.1448 0 0);
--foreground: oklch(0.9851 0 0);
--card: oklch(0.2134 0 0);
--secondary: oklch(0.2686 0 0);
--muted-foreground: oklch(0.7090 0 0);
--border: oklch(0.3407 0 0);
--input: oklch(0.4386 0 0);
--ring: oklch(0.5555 0 0);
--link-color: oklch(0.7090 0 0);
--scrollbar-thumb: oklch(0.4386 0 0);
--scrollbar-track: transparent;
--inline-code-bg: oklch(0.2686 0 0);
}
/* Styled scrollbars */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--scrollbar-track);
}
::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb);
border-radius: 0;
}
::-webkit-scrollbar-thumb:hover {
background: #6e7681;
}
::-webkit-scrollbar-corner {
background: var(--scrollbar-track);
}
/* Firefox scrollbar */
* {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
body {
background: var(--background);
color: var(--foreground);
font-family: 'Geist Mono', ui-monospace, monospace;
}
.file-block {
border: 1px solid var(--border);
border-radius: 0;
overflow: hidden;
margin-bottom: 16px;
}
.file-header {
background: var(--card);
padding: 8px 16px;
border-bottom: 1px solid var(--border);
font-size: 14px;
font-family: 'Geist Mono', ui-monospace, monospace;
color: var(--muted-foreground);
}
.file-content {
background: var(--background);
overflow-x: auto;
}
.file-content pre {
margin: 0;
padding: 16px;
font-size: 13px;
line-height: 1.45;
}
.shiki {
background: transparent !important;
}
/* Markdown styles */
.markdown-body {
padding: 16px;
font-size: 14px;
line-height: 1.6;
}
.markdown-body h1, .markdown-body h2, .markdown-body h3 {
border-bottom: 1px solid var(--border);
padding-bottom: 0.3em;
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
}
.markdown-body h1 { font-size: 2em; }
.markdown-body h2 { font-size: 1.5em; }
.markdown-body h3 { font-size: 1.25em; }
.markdown-body p { margin-bottom: 16px; }
.markdown-body code {
background: var(--inline-code-bg);
padding: 0.2em 0.4em;
border-radius: 0;
font-size: 85%;
font-family: 'Geist Mono', ui-monospace, monospace;
}
.markdown-body pre {
background: var(--card);
padding: 16px;
border-radius: 0;
overflow-x: auto;
margin-bottom: 16px;
}
.markdown-body pre code {
background: transparent;
padding: 0;
font-size: 100%;
}
.markdown-body a {
color: var(--link-color);
text-decoration: none;
}
.markdown-body a:hover {
text-decoration: underline;
}
.markdown-body ul, .markdown-body ol {
padding-left: 2em;
margin-bottom: 16px;
}
.markdown-body li {
margin-bottom: 4px;
}
.markdown-body blockquote {
border-left: 4px solid var(--border);
padding-left: 16px;
color: var(--muted-foreground);
margin-bottom: 16px;
}
.view-list-item {
border: 1px solid var(--border);
border-radius: 0;
padding: 16px;
margin-bottom: 12px;
background: var(--card);
}
.view-list-item:hover {
border-color: var(--muted-foreground);
}
.view-list-item a {
color: var(--link-color);
text-decoration: none;
font-weight: 600;
}
.view-list-item a:hover {
text-decoration: underline;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
color: var(--muted-foreground);
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module">
import React, { useState, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
import { createHighlighter } from 'shiki';
import { marked } from 'marked';
// Map detected language names to Shiki language IDs
function mapDetectedToShiki(lang) {
const map = {
'shell': 'bash',
'sh': 'bash',
'c++': 'cpp',
'c#': 'csharp',
'objective-c': 'objc',
'actionscript 3': 'actionscript-3',
'just': 'makefile', // close enough
};
return map[lang] || lang;
}
// Get file extension for syntax highlighting
function getLanguage(filename, detectedLang) {
// Use pre-detected language if available
if (detectedLang) {
return mapDetectedToShiki(detectedLang);
}
// Fall back to extension-based detection
const ext = filename.split('.').pop()?.toLowerCase();
const langMap = {
'js': 'javascript',
'ts': 'typescript',
'tsx': 'tsx',
'jsx': 'jsx',
'py': 'python',
'rb': 'ruby',
'rs': 'rust',
'go': 'go',
'java': 'java',
'kt': 'kotlin',
'swift': 'swift',
'c': 'c',
'cpp': 'cpp',
'h': 'c',
'hpp': 'cpp',
'cs': 'csharp',
'php': 'php',
'sh': 'bash',
'bash': 'bash',
'zsh': 'bash',
'fish': 'fish',
'ps1': 'powershell',
'sql': 'sql',
'json': 'json',
'yaml': 'yaml',
'yml': 'yaml',
'xml': 'xml',
'html': 'html',
'css': 'css',
'scss': 'scss',
'less': 'less',
'md': 'markdown',
'toml': 'toml',
'ini': 'ini',
'dockerfile': 'dockerfile',
'makefile': 'makefile',
'lua': 'lua',
'vim': 'viml',
'ex': 'elixir',
'exs': 'elixir',
'erl': 'erlang',
'hs': 'haskell',
'ml': 'ocaml',
'clj': 'clojure',
'lisp': 'lisp',
'scala': 'scala',
'r': 'r',
'jl': 'julia',
'dart': 'dart',
'zig': 'zig',
'nim': 'nim',
'v': 'v',
'vue': 'vue',
'svelte': 'svelte',
};
return langMap[ext] || 'text';
}
function isMarkdown(filename) {
const ext = filename.split('.').pop()?.toLowerCase();
return ext === 'md' || ext === 'markdown';
}
// Highlighter singleton
let highlighterPromise = null;
async function getHighlighter() {
if (!highlighterPromise) {
highlighterPromise = createHighlighter({
themes: ['min-dark'],
langs: ['javascript', 'typescript', 'python', 'bash', 'json', 'yaml', 'html', 'css', 'rust', 'go', 'java', 'ruby', 'php', 'c', 'cpp', 'markdown', 'sql', 'text'],
});
}
return highlighterPromise;
}
function FileBlock({ file }) {
const [html, setHtml] = useState(null);
useEffect(() => {
async function highlight() {
if (isMarkdown(file.name)) {
const rendered = await marked(file.content);
setHtml({ type: 'markdown', content: rendered });
} else {
try {
const highlighter = await getHighlighter();
const lang = getLanguage(file.name, file.language);
// Load language if not already loaded
const loadedLangs = highlighter.getLoadedLanguages();
if (!loadedLangs.includes(lang) && lang !== 'text') {
try {
await highlighter.loadLanguage(lang);
} catch {
// Fall back to text if language not available
}
}
const highlighted = highlighter.codeToHtml(file.content, {
lang: loadedLangs.includes(lang) ? lang : 'text',
theme: 'min-dark',
});
setHtml({ type: 'code', content: highlighted });
} catch (err) {
setHtml({ type: 'code', content: '<pre>' + escapeHtml(file.content) + '</pre>' });
}
}
}
highlight();
}, [file]);
function escapeHtml(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
return React.createElement('div', { className: 'file-block' },
React.createElement('div', { className: 'file-header' }, file.display_path),
React.createElement('div', {
className: html?.type === 'markdown' ? 'markdown-body' : 'file-content',
dangerouslySetInnerHTML: html ? { __html: html.content } : undefined
}, html ? null : 'Loading...')
);
}
function ViewDisplay({ viewId }) {
const [view, setView] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/view/' + viewId)
.then(res => res.json())
.then(data => {
if (data.error) {
setError(data.error);
} else {
setView(data);
}
setLoading(false);
})
.catch(err => {
setError('Failed to load view');
setLoading(false);
});
}, [viewId]);
if (loading) {
return React.createElement('div', { className: 'loading' }, 'Loading...');
}
if (error) {
return React.createElement('div', { className: 'max-w-4xl mx-auto p-4' },
React.createElement('div', { className: 'text-gray-400' }, error)
);
}
return React.createElement('div', { className: 'max-w-4xl mx-auto p-4' },
React.createElement('div', { className: 'mb-4 flex items-center justify-between' },
React.createElement('a', { href: '/', className: 'text-gray-400 hover:underline' }, '← All views'),
React.createElement('span', { className: 'text-gray-500 text-sm' },
'Created: ' + new Date(view.created).toLocaleString()
)
),
view.files.map((file, i) =>
React.createElement(FileBlock, { key: i, file })
)
);
}
function ViewList() {
const [views, setViews] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/views')
.then(res => res.json())
.then(data => {
setViews(data);
setLoading(false);
})
.catch(() => setLoading(false));
}, []);
if (loading) {
return React.createElement('div', { className: 'loading' }, 'Loading...');
}
if (views.length === 0) {
return React.createElement('div', { className: 'max-w-4xl mx-auto p-4' },
React.createElement('h1', { className: 'text-2xl font-bold mb-4' }, 'viewctl'),
React.createElement('p', { className: 'text-gray-500' },
'No views yet. Create one with: viewctl create-view <files...>'
)
);
}
return React.createElement('div', { className: 'max-w-4xl mx-auto p-4' },
React.createElement('h1', { className: 'text-2xl font-bold mb-4' }, 'viewctl'),
views.map(view =>
React.createElement('div', { key: view.id, className: 'view-list-item' },
React.createElement('a', { href: '/view/' + view.id }, view.id),
React.createElement('div', { className: 'text-sm text-gray-500 mt-1' },
view.files?.length + ' file(s) • ' + new Date(view.created).toLocaleString()
),
React.createElement('div', { className: 'text-sm text-gray-400 mt-1' },
view.files?.map(f => f.display_path).join(', ')
)
)
)
);
}
function App() {
const path = window.location.pathname;
const viewMatch = path.match(/^\\/view\\/([a-z0-9]+)$/);
if (viewMatch) {
return React.createElement(ViewDisplay, { viewId: viewMatch[1] });
}
return React.createElement(ViewList);
}
const root = createRoot(document.getElementById('root'));
root.render(React.createElement(App));
</script>
</body>
</html>`;
const server = Bun.serve({
port,
async fetch(req) {
const url = new URL(req.url);
const path = url.pathname;
// API: List views
if (path === "/api/views") {
const views = await listViews();
return Response.json(views);
}
// API: Get single view with contents
const viewMatch = path.match(/^\/api\/view\/([a-z0-9]+)$/);
if (viewMatch) {
const view = await getViewWithContents(viewMatch[1]);
if (!view) {
return Response.json({ error: "View not found" }, { status: 404 });
}
return Response.json(view);
}
// Serve HTML for all other routes (SPA)
return new Response(HTML_TEMPLATE, {
headers: { "Content-Type": "text/html" },
});
},
});
console.log(`viewctl server running at http://localhost:${server.port}`);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment