fix: harden default co-author opt-out and correct the docs

Follow-up on the co-author default in this PR.

- Remove the existsSync/writeFileSync race in the installer settings write
  (CodeQL js/file-system-race, high). A single guarded read now covers the
  fresh-install case, and unreadable or non-object settings are left untouched.
- Respect `attribution` as an explicit user choice. It supersedes
  `includeCoAuthoredBy` in Claude Code 2.1.x, so a user who configured it would
  otherwise have had a dead key written into their settings.
- Share one opt-out rule via scripts/lib/claude-commit-attribution.js instead of
  duplicating it across the installer and plugin setup.
- Update the git-workflow rule and its nine mirrors and translations, which
  still told users ECC does not ship this setting.

We keep writing the deprecated `includeCoAuthoredBy` key rather than
`attribution`: unknown keys fail Claude Code settings validation, so writing
`attribution` would break users on older versions.
This commit is contained in:
haelyra
2026-08-10 17:48:33 -04:00
parent ea8f984be9
commit 14809cae9b
16 changed files with 210 additions and 30 deletions
+20 -10
View File
@@ -4,6 +4,10 @@ const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const {
hasExplicitCommitAttributionPreference,
withCommitAttributionDisabled,
} = require('../claude-commit-attribution');
const { writeInstallState } = require('../install-state');
const { filterMcpConfig, parseDisabledMcpServers } = require('../mcp-config');
const { assertWithinTrustedRoot } = require('../path-safety');
@@ -120,26 +124,32 @@ function shouldSetClaudeCommitAttributionPreference(plan) {
}
function writeClaudeCommitAttributionPreference(settingsPath) {
let settings = {};
if (fs.existsSync(settingsPath)) {
try {
settings = readJsonObject(settingsPath, 'Claude settings');
} catch (_error) {
// Read once rather than probing with existsSync first. Checking for the file and
// then writing it is a file system race (CodeQL js/file-system-race), and a
// missing file is simply the fresh-install case.
let settings;
try {
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
} catch (error) {
if (error.code !== 'ENOENT') {
// Unreadable or malformed settings belong to the user; leave them untouched.
return false;
}
settings = {};
}
if (settings.includeCoAuthoredBy === true) {
if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
return false;
}
if (hasExplicitCommitAttributionPreference(settings)) {
return false;
}
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
fs.writeFileSync(
settingsPath,
formatJson({
...settings,
includeCoAuthoredBy: false,
}),
formatJson(withCommitAttributionDisabled(settings)),
'utf8'
);
return true;