diff --git a/bin/install.js b/bin/install.js
index cff9bb5f..95b50d7e 100755
--- a/bin/install.js
+++ b/bin/install.js
@@ -2606,10 +2606,14 @@ function install(isGlobal, runtime = 'claude') {
if (fs.statSync(srcFile).isFile()) {
const destFile = path.join(hooksDest, entry);
// Template .js files to replace '.claude' with runtime-specific config dir
+ // and stamp the current GSD version into the hook version header
if (entry.endsWith('.js')) {
let content = fs.readFileSync(srcFile, 'utf8');
content = content.replace(/'\.claude'/g, configDirReplacement);
+ content = content.replace(/\{\{GSD_VERSION\}\}/g, pkg.version);
fs.writeFileSync(destFile, content);
+ // Ensure hook files are executable (fixes #1162 — missing +x permission)
+ try { fs.chmodSync(destFile, 0o755); } catch (e) { /* Windows doesn't support chmod */ }
} else {
fs.copyFileSync(srcFile, destFile);
}
diff --git a/commands/gsd/session-report.md b/commands/gsd/session-report.md
new file mode 100644
index 00000000..a0eb1d6e
--- /dev/null
+++ b/commands/gsd/session-report.md
@@ -0,0 +1,19 @@
+---
+name: gsd:session-report
+description: Generate a session report with token usage estimates, work summary, and outcomes
+allowed-tools:
+ - Read
+ - Bash
+ - Write
+---
+
+Generate a structured SESSION_REPORT.md document capturing session outcomes, work performed, and estimated resource usage. Provides a shareable artifact for post-session review.
+
+
+
+@~/.claude/get-shit-done/workflows/session-report.md
+
+
+
+Execute the session-report workflow from @~/.claude/get-shit-done/workflows/session-report.md end-to-end.
+
diff --git a/get-shit-done/workflows/session-report.md b/get-shit-done/workflows/session-report.md
new file mode 100644
index 00000000..f336edc0
--- /dev/null
+++ b/get-shit-done/workflows/session-report.md
@@ -0,0 +1,146 @@
+
+Generate a post-session summary document capturing work performed, outcomes achieved, and estimated resource usage. Writes SESSION_REPORT.md to .planning/reports/ for human review and stakeholder sharing.
+
+
+
+Read all files referenced by the invoking prompt's execution_context before starting.
+
+
+
+
+
+Collect session data from available sources:
+
+1. **STATE.md** — current phase, milestone, progress, blockers, decisions
+2. **Git log** — commits made during this session (last 24h or since last report)
+3. **Plan/Summary files** — plans executed, summaries written
+4. **ROADMAP.md** — milestone context and phase goals
+
+```bash
+# Get recent commits (last 24 hours)
+git log --oneline --since="24 hours ago" --no-merges 2>/dev/null || echo "No recent commits"
+
+# Count files changed
+git diff --stat HEAD~10 HEAD 2>/dev/null | tail -1 || echo "No diff available"
+```
+
+Read `.planning/STATE.md` to get:
+- Current milestone and phase
+- Progress percentage
+- Active blockers
+- Recent decisions
+
+Read `.planning/ROADMAP.md` to get milestone name and goals.
+
+Check for existing reports:
+```bash
+ls -la .planning/reports/SESSION_REPORT*.md 2>/dev/null || echo "No previous reports"
+```
+
+
+
+Estimate token usage from observable signals:
+
+- Count of tool calls is not directly available, so estimate from git activity and file operations
+- Note: This is an **estimate** — exact token counts require API-level instrumentation not available to hooks
+
+Estimation heuristics:
+- Each commit ≈ 1 plan cycle (research + plan + execute + verify)
+- Each plan file ≈ 2,000-5,000 tokens of agent context
+- Each summary file ≈ 1,000-2,000 tokens generated
+- Subagent spawns multiply by ~1.5x per agent type used
+
+
+
+Create the report directory and file:
+
+```bash
+mkdir -p .planning/reports
+```
+
+Write `.planning/reports/SESSION_REPORT.md` (or `.planning/reports/YYYYMMDD-session-report.md` if previous reports exist):
+
+```markdown
+# GSD Session Report
+
+**Generated:** [timestamp]
+**Project:** [from PROJECT.md title or directory name]
+**Milestone:** [N] — [milestone name from ROADMAP.md]
+
+---
+
+## Session Summary
+
+**Duration:** [estimated from first to last commit timestamp, or "Single session"]
+**Phase Progress:** [from STATE.md]
+**Plans Executed:** [count of summaries written this session]
+**Commits Made:** [count from git log]
+
+## Work Performed
+
+### Phases Touched
+[List phases worked on with brief description of what was done]
+
+### Key Outcomes
+[Bullet list of concrete deliverables: files created, features implemented, bugs fixed]
+
+### Decisions Made
+[From STATE.md decisions table, if any were added this session]
+
+## Files Changed
+
+[Summary of files modified, created, deleted — from git diff stat]
+
+## Blockers & Open Items
+
+[Active blockers from STATE.md]
+[Any TODO items created during session]
+
+## Estimated Resource Usage
+
+| Metric | Estimate |
+|--------|----------|
+| Commits | [N] |
+| Files changed | [N] |
+| Plans executed | [N] |
+| Subagents spawned | [estimated] |
+
+> **Note:** Token and cost estimates require API-level instrumentation.
+> These metrics reflect observable session activity only.
+
+---
+
+*Generated by `/gsd:session-report`*
+```
+
+
+
+Show the user:
+
+```
+## Session Report Generated
+
+📄 `.planning/reports/[filename].md`
+
+### Highlights
+- **Commits:** [N]
+- **Files changed:** [N]
+- **Phase progress:** [X]%
+- **Plans executed:** [N]
+```
+
+If this is the first report, mention:
+```
+💡 Run `/gsd:session-report` at the end of each session to build a history of project activity.
+```
+
+
+
+
+
+- [ ] Session data gathered from STATE.md, git log, and plan files
+- [ ] Report written to .planning/reports/
+- [ ] Report includes work summary, outcomes, and file changes
+- [ ] Filename includes date to prevent overwrites
+- [ ] Result summary displayed to user
+
diff --git a/hooks/gsd-check-update.js b/hooks/gsd-check-update.js
index b9a6075e..510302fb 100755
--- a/hooks/gsd-check-update.js
+++ b/hooks/gsd-check-update.js
@@ -1,4 +1,5 @@
#!/usr/bin/env node
+// gsd-hook-version: {{GSD_VERSION}}
// Check for GSD updates in background, write result to cache
// Called by SessionStart hook - runs once per session
@@ -43,6 +44,7 @@ if (!fs.existsSync(cacheDir)) {
// Run check in background (spawn background process, windowsHide prevents console flash)
const child = spawn(process.execPath, ['-e', `
const fs = require('fs');
+ const path = require('path');
const { execSync } = require('child_process');
const cacheFile = ${JSON.stringify(cacheFile)};
@@ -51,14 +53,43 @@ const child = spawn(process.execPath, ['-e', `
// Check project directory first (local install), then global
let installed = '0.0.0';
+ let configDir = '';
try {
if (fs.existsSync(projectVersionFile)) {
installed = fs.readFileSync(projectVersionFile, 'utf8').trim();
+ configDir = path.dirname(path.dirname(projectVersionFile));
} else if (fs.existsSync(globalVersionFile)) {
installed = fs.readFileSync(globalVersionFile, 'utf8').trim();
+ configDir = path.dirname(path.dirname(globalVersionFile));
}
} catch (e) {}
+ // Check for stale hooks — compare hook version headers against installed VERSION
+ let staleHooks = [];
+ if (configDir) {
+ const hooksDir = path.join(configDir, 'hooks');
+ try {
+ if (fs.existsSync(hooksDir)) {
+ const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.js'));
+ for (const hookFile of hookFiles) {
+ try {
+ const content = fs.readFileSync(path.join(hooksDir, hookFile), 'utf8');
+ const versionMatch = content.match(/\\/\\/ gsd-hook-version:\\s*(.+)/);
+ if (versionMatch) {
+ const hookVersion = versionMatch[1].trim();
+ if (hookVersion !== installed && !hookVersion.includes('{{')) {
+ staleHooks.push({ file: hookFile, hookVersion, installedVersion: installed });
+ }
+ } else {
+ // No version header at all — definitely stale (pre-version-tracking)
+ staleHooks.push({ file: hookFile, hookVersion: 'unknown', installedVersion: installed });
+ }
+ } catch (e) {}
+ }
+ }
+ } catch (e) {}
+ }
+
let latest = null;
try {
latest = execSync('npm view get-shit-done-cc version', { encoding: 'utf8', timeout: 10000, windowsHide: true }).trim();
@@ -68,7 +99,8 @@ const child = spawn(process.execPath, ['-e', `
update_available: latest && installed !== latest,
installed,
latest: latest || 'unknown',
- checked: Math.floor(Date.now() / 1000)
+ checked: Math.floor(Date.now() / 1000),
+ stale_hooks: staleHooks.length > 0 ? staleHooks : undefined
};
fs.writeFileSync(cacheFile, JSON.stringify(result));
diff --git a/hooks/gsd-context-monitor.js b/hooks/gsd-context-monitor.js
index d7a5eff0..ae1bbf9a 100644
--- a/hooks/gsd-context-monitor.js
+++ b/hooks/gsd-context-monitor.js
@@ -1,4 +1,5 @@
#!/usr/bin/env node
+// gsd-hook-version: {{GSD_VERSION}}
// Context Monitor - PostToolUse/AfterTool hook (Gemini uses AfterTool)
// Reads context metrics from the statusline bridge file and injects
// warnings when context usage is high. This makes the AGENT aware of
@@ -27,10 +28,11 @@ const STALE_SECONDS = 60; // ignore metrics older than 60s
const DEBOUNCE_CALLS = 5; // min tool uses between warnings
let input = '';
-// Timeout guard: if stdin doesn't close within 3s (e.g. pipe issues on
-// Windows/Git Bash), exit silently instead of hanging until Claude Code
-// kills the process and reports "hook error". See #775.
-const stdinTimeout = setTimeout(() => process.exit(0), 3000);
+// Timeout guard: if stdin doesn't close within 10s (e.g. pipe issues on
+// Windows/Git Bash, or slow Claude Code piping during large outputs),
+// exit silently instead of hanging until Claude Code kills the process
+// and reports "hook error". See #775, #1162.
+const stdinTimeout = setTimeout(() => process.exit(0), 10000);
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => input += chunk);
process.stdin.on('end', () => {
diff --git a/hooks/gsd-statusline.js b/hooks/gsd-statusline.js
index d88ca4a2..ae7025b9 100755
--- a/hooks/gsd-statusline.js
+++ b/hooks/gsd-statusline.js
@@ -1,4 +1,5 @@
#!/usr/bin/env node
+// gsd-hook-version: {{GSD_VERSION}}
// Claude Code Statusline - GSD Edition
// Shows: model | current task | directory | context usage
@@ -99,6 +100,9 @@ process.stdin.on('end', () => {
if (cache.update_available) {
gsdUpdate = '\x1b[33m⬆ /gsd:update\x1b[0m │ ';
}
+ if (cache.stale_hooks && cache.stale_hooks.length > 0) {
+ gsdUpdate += '\x1b[31m⚠ stale hooks — run /gsd:update\x1b[0m │ ';
+ }
} catch (e) {}
}