diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs
index 90506e131..f2599c010 100755
--- a/get-shit-done/bin/gsd-tools.cjs
+++ b/get-shit-done/bin/gsd-tools.cjs
@@ -639,6 +639,11 @@ async function runCommand(command, args, cwd, raw, defaultValue) {
break;
}
+ case 'config-path': {
+ config.cmdConfigPath(cwd, raw);
+ break;
+ }
+
case 'agent-skills': {
init.cmdAgentSkills(cwd, args[1], raw);
break;
diff --git a/get-shit-done/bin/lib/config.cjs b/get-shit-done/bin/lib/config.cjs
index 3d532a7bb..80b875f6e 100644
--- a/get-shit-done/bin/lib/config.cjs
+++ b/get-shit-done/bin/lib/config.cjs
@@ -495,6 +495,17 @@ function getCmdConfigSetModelProfileResultMessage(
return paragraphs.join('\n\n');
}
+/**
+ * Print the resolved config.json path (workstream-aware). Used by settings.md
+ * so the workflow writes/reads the correct file when a workstream is active (#2282).
+ */
+function cmdConfigPath(cwd) {
+ // Always emit as plain text — a file path is used via shell substitution,
+ // never consumed as JSON. Passing raw=true forces plain-text output.
+ const configPath = path.join(planningDir(cwd), 'config.json');
+ output(configPath, true, configPath);
+}
+
module.exports = {
VALID_CONFIG_KEYS,
cmdConfigEnsureSection,
@@ -502,4 +513,5 @@ module.exports = {
cmdConfigGet,
cmdConfigSetModelProfile,
cmdConfigNewProject,
+ cmdConfigPath,
};
diff --git a/get-shit-done/workflows/settings.md b/get-shit-done/workflows/settings.md
index 503460def..aa429af68 100644
--- a/get-shit-done/workflows/settings.md
+++ b/get-shit-done/workflows/settings.md
@@ -13,16 +13,18 @@ Ensure config exists and load current state:
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-ensure-section
+GSD_CONFIG_PATH=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-path)
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state load)
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
```
-Creates `.planning/config.json` with defaults if missing and loads current config values.
+Creates config.json (at the workstream-aware path) with defaults if missing and loads current config values.
+Store `$GSD_CONFIG_PATH` — all subsequent reads and writes use this path, not the hardcoded `.planning/config.json`, so active-workstream installs write to the correct location (#2282).
```bash
-cat .planning/config.json
+cat "$GSD_CONFIG_PATH"
```
Parse current values (default to `true` if not present):
@@ -213,7 +215,7 @@ Merge new settings into existing config.json:
}
```
-Write updated config to `.planning/config.json`.
+Write updated config to `$GSD_CONFIG_PATH` (the workstream-aware path resolved in `ensure_and_load_config`). Never hardcode `.planning/config.json` — workstream installs route to `.planning/workstreams//config.json`.
diff --git a/tests/config.test.cjs b/tests/config.test.cjs
index 8651eedd2..5bee2988e 100644
--- a/tests/config.test.cjs
+++ b/tests/config.test.cjs
@@ -935,3 +935,41 @@ describe('config-set/config-get context', () => {
assert.ok(fs.existsSync(path.join(contextsDir, 'review.md')), 'review.md should exist');
});
});
+
+// ─── config-path (#2282) ────────────────────────────────────────────────────
+
+describe('config-path command (#2282)', () => {
+ let tmpDir;
+
+ beforeEach(() => {
+ tmpDir = createTempProject();
+ runGsdTools('config-ensure-section', tmpDir);
+ });
+
+ afterEach(() => {
+ cleanup(tmpDir);
+ });
+
+ test('returns root config path when no workstream is active', () => {
+ const result = runGsdTools('config-path', tmpDir);
+ assert.ok(result.success, `config-path failed: ${result.error}`);
+ assert.ok(result.output.trim().endsWith('.planning/config.json'), `expected root config path, got: ${result.output}`);
+ assert.ok(!result.output.includes('workstreams'), 'should not include workstreams in path');
+ });
+
+ test('returns workstream config path when GSD_WORKSTREAM is set', () => {
+ const result = runGsdTools('config-path', tmpDir, { GSD_WORKSTREAM: 'my-stream' });
+ assert.ok(result.success, `config-path failed: ${result.error}`);
+ assert.ok(result.output.trim().includes('workstreams/my-stream/config.json'), `expected workstream config path, got: ${result.output}`);
+ });
+
+ test('config-path and config-get agree on the active path', () => {
+ // Write a value via config-set (uses planningDir internally)
+ runGsdTools('config-set model_profile quality', tmpDir);
+ // config-path should point to a file containing that value
+ const pathResult = runGsdTools('config-path', tmpDir);
+ const configPath = pathResult.output.trim();
+ const configContent = JSON.parse(require('fs').readFileSync(configPath, 'utf-8'));
+ assert.strictEqual(configContent.model_profile, 'quality', 'config-path should point to the file config-set wrote');
+ });
+});