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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Fixed
- Local install: workflow and reference files now get absolute path to `gsd-tools.cjs` instead of `$HOME/.claude/get-shit-done/`, so local installs work when GSD is outside `$HOME` and spawned subagents with empty `$HOME` still resolve the path (#820)

## [1.22.4] - 2026-03-03

### Added
Expand Down
44 changes: 14 additions & 30 deletions bin/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,6 @@ if (hasAll) {
if (hasCodex) selectedRuntimes.push('codex');
}

/**
* Convert a pathPrefix (which uses absolute paths for global installs) to a
* $HOME-relative form for replacing $HOME/.claude/ references in bash code blocks.
* Preserves $HOME as a shell variable so paths remain portable across machines.
*/
function toHomePrefix(pathPrefix) {
const home = os.homedir().replace(/\\/g, '/');
const normalized = pathPrefix.replace(/\\/g, '/');
if (normalized.startsWith(home)) {
return '$HOME' + normalized.slice(home.length);
}
// For relative paths or paths not under $HOME, return as-is
return normalized;
}

// Helper to get directory name for a runtime (used for local/project installs)
function getDirName(runtime) {
if (runtime === 'opencode') return '.opencode';
Expand Down Expand Up @@ -715,14 +700,14 @@ function installCodexConfig(targetDir, agentsSrc) {
const agentEntries = fs.readdirSync(agentsSrc).filter(f => f.startsWith('gsd-') && f.endsWith('.md'));
const agents = [];

// Compute the Codex pathPrefix for replacing .claude paths
const codexPathPrefix = `${targetDir.replace(/\\/g, '/')}/`;
// Compute the Codex GSD install path (absolute, so subagents with empty $HOME work — #820)
const codexGsdPath = `${path.resolve(targetDir, 'get-shit-done').replace(/\\/g, '/')}/`;

for (const file of agentEntries) {
let content = fs.readFileSync(path.join(agentsSrc, file), 'utf8');
// Replace .claude paths before generating TOML (source files use ~/.claude and $HOME/.claude)
content = content.replace(/~\/\.claude\//g, codexPathPrefix);
content = content.replace(/\$HOME\/\.claude\//g, toHomePrefix(codexPathPrefix));
// Replace full .claude/get-shit-done prefix so path resolves to codex GSD install
content = content.replace(/~\/\.claude\/get-shit-done\//g, codexGsdPath);
content = content.replace(/\$HOME\/\.claude\/get-shit-done\//g, codexGsdPath);
const { frontmatter } = extractFrontmatterAndBody(content);
const name = extractFrontmatterField(frontmatter, 'name') || file.replace('.md', '');
const description = extractFrontmatterField(frontmatter, 'description') || '';
Expand Down Expand Up @@ -1032,7 +1017,7 @@ function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
const localClaudeRegex = /\.\/\.claude\//g;
const opencodeDirRegex = /~\/\.opencode\//g;
content = content.replace(globalClaudeRegex, pathPrefix);
content = content.replace(globalClaudeHomeRegex, toHomePrefix(pathPrefix));
content = content.replace(globalClaudeHomeRegex, pathPrefix);
content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
content = content.replace(opencodeDirRegex, pathPrefix);
content = processAttribution(content, getCommitAttribution(runtime));
Expand Down Expand Up @@ -1093,7 +1078,7 @@ function copyCommandsAsCodexSkills(srcDir, skillsDir, prefix, pathPrefix, runtim
const localClaudeRegex = /\.\/\.claude\//g;
const codexDirRegex = /~\/\.codex\//g;
content = content.replace(globalClaudeRegex, pathPrefix);
content = content.replace(globalClaudeHomeRegex, toHomePrefix(pathPrefix));
content = content.replace(globalClaudeHomeRegex, pathPrefix);
content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
content = content.replace(codexDirRegex, pathPrefix);
content = processAttribution(content, getCommitAttribution(runtime));
Expand Down Expand Up @@ -1140,7 +1125,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
const localClaudeRegex = /\.\/\.claude\//g;
content = content.replace(globalClaudeRegex, pathPrefix);
content = content.replace(globalClaudeHomeRegex, toHomePrefix(pathPrefix));
content = content.replace(globalClaudeHomeRegex, pathPrefix);
content = content.replace(localClaudeRegex, `./${dirName}/`);
content = processAttribution(content, getCommitAttribution(runtime));

Expand Down Expand Up @@ -1888,12 +1873,11 @@ function install(isGlobal, runtime = 'claude') {
? targetDir.replace(os.homedir(), '~')
: targetDir.replace(process.cwd(), '.');

// Path prefix for file references in markdown content
// For global installs: use full path
// For local installs: use relative
const pathPrefix = isGlobal
? `${targetDir.replace(/\\/g, '/')}/`
: `./${dirName}/`;
// Path prefix for file references in markdown content (e.g. gsd-tools.cjs).
// Replaces $HOME/.claude/ or ~/.claude/ so the result is <pathPrefix>get-shit-done/bin/...
// Always use absolute path so: (1) local installs work when GSD is outside $HOME,
// (2) spawned subagents with empty $HOME still resolve the path (fixes #820).
const pathPrefix = `${path.resolve(targetDir).replace(/\\/g, '/')}/`;

let runtimeLabel = 'Claude Code';
if (isOpencode) runtimeLabel = 'OpenCode';
Expand Down Expand Up @@ -1985,7 +1969,7 @@ function install(isGlobal, runtime = 'claude') {
const dirRegex = /~\/\.claude\//g;
const homeDirRegex = /\$HOME\/\.claude\//g;
content = content.replace(dirRegex, pathPrefix);
content = content.replace(homeDirRegex, toHomePrefix(pathPrefix));
content = content.replace(homeDirRegex, pathPrefix);
content = processAttribution(content, getCommitAttribution(runtime));
// Convert frontmatter for runtime compatibility
if (isOpencode) {
Expand Down