mirror of
https://github.com/affaan-m/ECC.git
synced 2026-08-17 21:15:40 +02:00
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:
@@ -13,7 +13,7 @@ alwaysApply: true
|
||||
|
||||
Types: feat, fix, refactor, docs, test, chore, perf, ci
|
||||
|
||||
Note: To disable co-author attribution on commits, set `"includeCoAuthoredBy": false` in `~/.claude/settings.json` (Claude Code appends `Co-Authored-By` by default; ECC does not ship this setting).
|
||||
Note: ECC-managed installs set `"includeCoAuthoredBy": false` in `~/.claude/settings.json`, so commits carry no `Co-Authored-By` trailer by default. To keep Claude attribution, set `"includeCoAuthoredBy": true` or configure `attribution`; ECC never overwrites an explicit choice.
|
||||
|
||||
## Pull Request Workflow
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ description: Git workflow guidelines for conventional commits and pull request p
|
||||
|
||||
Types: feat, fix, refactor, docs, test, chore, perf, ci
|
||||
|
||||
Note: To disable co-author attribution on commits, set `"includeCoAuthoredBy": false` in `~/.claude/settings.json` (Claude Code appends `Co-Authored-By` by default; ECC does not ship this setting).
|
||||
Note: ECC-managed installs set `"includeCoAuthoredBy": false` in `~/.claude/settings.json`, so commits carry no `Co-Authored-By` trailer by default. To keep Claude attribution, set `"includeCoAuthoredBy": true` or configure `attribution`; ECC never overwrites an explicit choice.
|
||||
|
||||
## Pull Request Workflow
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
Tipos: feat, fix, refactor, docs, test, chore, perf, ci
|
||||
|
||||
Nota: Para desactivar la atribución de coautoría, configure `"includeCoAuthoredBy": false` en `~/.claude/settings.json`; Claude Code agrega `Co-Authored-By` de forma predeterminada y ECC no incluye esta configuración.
|
||||
Nota: Las instalaciones gestionadas por ECC configuran `"includeCoAuthoredBy": false` en `~/.claude/settings.json`, por lo que los commits no incluyen `Co-Authored-By` de forma predeterminada. Para conservar la atribución de Claude, configure `"includeCoAuthoredBy": true` o `attribution`; ECC nunca sobrescribe una elección explícita.
|
||||
|
||||
## Flujo de Trabajo de Pull Request
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
タイプ: feat, fix, refactor, docs, test, chore, perf, ci
|
||||
|
||||
注記: コミットの共同作成者の属性を無効にするには、`~/.claude/settings.json` に `"includeCoAuthoredBy": false` を設定します。Claude Code は既定で `Co-Authored-By` を付与し、ECC はこの設定を同梱しません。
|
||||
注記: ECC が管理するインストールは `~/.claude/settings.json` に `"includeCoAuthoredBy": false` を設定するため、コミットには既定で `Co-Authored-By` が付きません。Claude の属性を残す場合は `"includeCoAuthoredBy": true` または `attribution` を設定してください。ECC は明示的な設定を上書きしません。
|
||||
|
||||
## Pull Request ワークフロー
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
타입: feat, fix, refactor, docs, test, chore, perf, ci
|
||||
|
||||
참고: 공동 작성자 표기를 비활성화하려면 `~/.claude/settings.json`에 `"includeCoAuthoredBy": false`를 설정하세요. Claude Code는 기본적으로 `Co-Authored-By`를 추가하며 ECC는 이 설정을 포함하지 않습니다.
|
||||
참고: ECC가 관리하는 설치는 `~/.claude/settings.json`에 `"includeCoAuthoredBy": false`를 설정하므로 커밋에 기본적으로 `Co-Authored-By`가 붙지 않습니다. Claude 표기를 유지하려면 `"includeCoAuthoredBy": true`를 설정하거나 `attribution`을 구성하세요. ECC는 명시적인 선택을 덮어쓰지 않습니다.
|
||||
|
||||
## Pull Request 워크플로우
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
Tipos: feat, fix, refactor, docs, test, chore, perf, ci
|
||||
|
||||
Nota: Para desativar a atribuição de coautoria, defina `"includeCoAuthoredBy": false` em `~/.claude/settings.json`; o Claude Code adiciona `Co-Authored-By` por padrão e o ECC não inclui essa configuração.
|
||||
Nota: As instalações gerenciadas pelo ECC definem `"includeCoAuthoredBy": false` em `~/.claude/settings.json`, portanto os commits não incluem `Co-Authored-By` por padrão. Para manter a atribuição do Claude, defina `"includeCoAuthoredBy": true` ou configure `attribution`; o ECC nunca sobrescreve uma escolha explícita.
|
||||
|
||||
## Fluxo de Trabalho de Pull Request
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
Types: feat, fix, refactor, docs, test, chore, perf, ci
|
||||
|
||||
Not: Ortak yazar atfını devre dışı bırakmak için `~/.claude/settings.json` içinde `"includeCoAuthoredBy": false` ayarlayın; Claude Code varsayılan olarak `Co-Authored-By` ekler ve ECC bu ayarı içermez.
|
||||
Not: ECC tarafından yönetilen kurulumlar `~/.claude/settings.json` içinde `"includeCoAuthoredBy": false` ayarlar, bu nedenle commitler varsayılan olarak `Co-Authored-By` içermez. Claude atfını korumak için `"includeCoAuthoredBy": true` veya `attribution` ayarlayın; ECC açık bir tercihin üzerine asla yazmaz.
|
||||
|
||||
## Pull Request İş Akışı
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
类型:feat, fix, refactor, docs, test, chore, perf, ci
|
||||
|
||||
注意:若要禁用共同作者归因,请在 `~/.claude/settings.json` 中设置 `"includeCoAuthoredBy": false`;Claude Code 默认会附加 `Co-Authored-By`,而 ECC 不会附带此设置。
|
||||
注意:ECC 管理的安装会在 `~/.claude/settings.json` 中设置 `"includeCoAuthoredBy": false`,因此提交默认不带 `Co-Authored-By`。若要保留 Claude 的归因,请设置 `"includeCoAuthoredBy": true` 或配置 `attribution`;ECC 不会覆盖用户的显式选择。
|
||||
|
||||
## 拉取请求工作流程
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
類型:feat、fix、refactor、docs、test、chore、perf、ci
|
||||
|
||||
注意:若要停用共同作者歸屬,請在 `~/.claude/settings.json` 中設定 `"includeCoAuthoredBy": false`;Claude Code 預設會附加 `Co-Authored-By`,而 ECC 不會隨附這個設定。
|
||||
注意:ECC 管理的安裝會在 `~/.claude/settings.json` 中設定 `"includeCoAuthoredBy": false`,因此提交預設不會附帶 `Co-Authored-By`。若要保留 Claude 的歸屬,請設定 `"includeCoAuthoredBy": true` 或設定 `attribution`;ECC 不會覆寫使用者的明確選擇。
|
||||
|
||||
## Pull Request 工作流程
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
Types: feat, fix, refactor, docs, test, chore, perf, ci
|
||||
|
||||
Note: To disable co-author attribution on commits, set `"includeCoAuthoredBy": false` in `~/.claude/settings.json` (Claude Code appends `Co-Authored-By` by default; ECC does not ship this setting).
|
||||
Note: ECC-managed installs set `"includeCoAuthoredBy": false` in `~/.claude/settings.json`, so commits carry no `Co-Authored-By` trailer by default. To keep Claude attribution, set `"includeCoAuthoredBy": true` or configure `attribution`; ECC never overwrites an explicit choice.
|
||||
|
||||
## Pull Request Workflow
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
// Claude Code appends a `Co-Authored-By` trailer to commits and PRs unless the
|
||||
// user opts out, so ECC-managed installs default that off.
|
||||
//
|
||||
// Two settings control the trailer. `attribution: { commit, pr }` is the current
|
||||
// one and wins when set; `includeCoAuthoredBy` is deprecated as of Claude Code
|
||||
// 2.1.x but still honored, and is the only one older versions understand. We
|
||||
// write the deprecated key because unknown keys fail settings validation, so
|
||||
// writing `attribution` would break users on older Claude Code. Either key being
|
||||
// present counts as a deliberate user choice that ECC must not overwrite.
|
||||
const COAUTHOR_SETTING_KEY = 'includeCoAuthoredBy';
|
||||
|
||||
function hasExplicitCommitAttributionPreference(settings) {
|
||||
if (!settings || typeof settings !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if (typeof settings[COAUTHOR_SETTING_KEY] === 'boolean') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const attribution = settings.attribution;
|
||||
return Boolean(attribution)
|
||||
&& typeof attribution === 'object'
|
||||
&& !Array.isArray(attribution)
|
||||
&& (attribution.commit !== undefined || attribution.pr !== undefined);
|
||||
}
|
||||
|
||||
function withCommitAttributionDisabled(settings) {
|
||||
if (hasExplicitCommitAttributionPreference(settings)) {
|
||||
return settings;
|
||||
}
|
||||
return {
|
||||
...settings,
|
||||
[COAUTHOR_SETTING_KEY]: false,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
COAUTHOR_SETTING_KEY,
|
||||
hasExplicitCommitAttributionPreference,
|
||||
withCommitAttributionDisabled,
|
||||
};
|
||||
@@ -5,6 +5,10 @@ const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const { writeFileAtomic } = require('./atomic-write');
|
||||
const {
|
||||
hasExplicitCommitAttributionPreference,
|
||||
withCommitAttributionDisabled,
|
||||
} = require('./claude-commit-attribution');
|
||||
const { normalizeGitHubGitOrigin } = require('./github-origin');
|
||||
const {
|
||||
CURRENT_PLUGIN_ID,
|
||||
@@ -18,7 +22,6 @@ const OFFICIAL_MARKETPLACE_NAME = 'ecc';
|
||||
const OFFICIAL_MARKETPLACE_REPO = 'affaan-m/ecc';
|
||||
const OFFICIAL_MARKETPLACE_URL = 'https://github.com/affaan-m/ECC';
|
||||
const PROVIDER_COMMAND_TIMEOUT_MS = 120 * 1000;
|
||||
const CLAUDE_COAUTHOR_SETTING_KEY = 'includeCoAuthoredBy';
|
||||
const VALID_SCOPES = new Set(['user', 'project', 'local']);
|
||||
const VALID_HOOK_MODES = new Set(['off', 'minimal', 'standard', 'strict']);
|
||||
|
||||
@@ -322,18 +325,11 @@ function deriveHookMode(settings) {
|
||||
}
|
||||
|
||||
function withClaudeCommitAttributionPreference(settings) {
|
||||
if (settings?.[CLAUDE_COAUTHOR_SETTING_KEY] === true) {
|
||||
return settings;
|
||||
}
|
||||
return {
|
||||
...settings,
|
||||
[CLAUDE_COAUTHOR_SETTING_KEY]: false,
|
||||
};
|
||||
return withCommitAttributionDisabled(settings);
|
||||
}
|
||||
|
||||
function needsClaudeCommitAttributionPreferenceWrite(settings) {
|
||||
return settings?.[CLAUDE_COAUTHOR_SETTING_KEY] !== false
|
||||
&& settings?.[CLAUDE_COAUTHOR_SETTING_KEY] !== true;
|
||||
return !hasExplicitCommitAttributionPreference(settings);
|
||||
}
|
||||
|
||||
function writeClaudePluginOptions(settingsPath, hooks) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const {
|
||||
hasExplicitCommitAttributionPreference,
|
||||
withCommitAttributionDisabled,
|
||||
} = require('../../scripts/lib/claude-commit-attribution');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` ✓ ${name}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(` ✗ ${name}`);
|
||||
console.log(` Error: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\nClaude commit attribution preference');
|
||||
|
||||
if (test('treats absent settings as unconfigured', () => {
|
||||
assert.strictEqual(hasExplicitCommitAttributionPreference(undefined), false);
|
||||
assert.strictEqual(hasExplicitCommitAttributionPreference(null), false);
|
||||
assert.strictEqual(hasExplicitCommitAttributionPreference({}), false);
|
||||
assert.strictEqual(hasExplicitCommitAttributionPreference({ theme: 'dark' }), false);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('treats either includeCoAuthoredBy boolean as an explicit choice', () => {
|
||||
assert.strictEqual(hasExplicitCommitAttributionPreference({ includeCoAuthoredBy: true }), true);
|
||||
assert.strictEqual(hasExplicitCommitAttributionPreference({ includeCoAuthoredBy: false }), true);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('treats a configured attribution as an explicit choice', () => {
|
||||
// `attribution` supersedes `includeCoAuthoredBy` in Claude Code, so a user who
|
||||
// set it has already decided and ECC must not write a key that loses to it.
|
||||
assert.strictEqual(hasExplicitCommitAttributionPreference({ attribution: { commit: '' } }), true);
|
||||
assert.strictEqual(hasExplicitCommitAttributionPreference({ attribution: { pr: '' } }), true);
|
||||
assert.strictEqual(
|
||||
hasExplicitCommitAttributionPreference({ attribution: { commit: 'Co-Authored-By: Someone <a@b.c>' } }),
|
||||
true
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('ignores attribution values that carry no commit or pr choice', () => {
|
||||
assert.strictEqual(hasExplicitCommitAttributionPreference({ attribution: {} }), false);
|
||||
assert.strictEqual(hasExplicitCommitAttributionPreference({ attribution: null }), false);
|
||||
assert.strictEqual(hasExplicitCommitAttributionPreference({ attribution: [] }), false);
|
||||
assert.strictEqual(hasExplicitCommitAttributionPreference({ attribution: 'off' }), false);
|
||||
assert.strictEqual(hasExplicitCommitAttributionPreference({ attribution: { sessionUrl: false } }), false);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('disables attribution while preserving unrelated settings', () => {
|
||||
assert.deepStrictEqual(
|
||||
withCommitAttributionDisabled({ theme: 'dark' }),
|
||||
{ theme: 'dark', includeCoAuthoredBy: false }
|
||||
);
|
||||
assert.deepStrictEqual(withCommitAttributionDisabled({}), { includeCoAuthoredBy: false });
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('returns explicit settings unchanged', () => {
|
||||
const optIn = { includeCoAuthoredBy: true };
|
||||
assert.strictEqual(withCommitAttributionDisabled(optIn), optIn);
|
||||
|
||||
const alreadyOff = { includeCoAuthoredBy: false };
|
||||
assert.strictEqual(withCommitAttributionDisabled(alreadyOff), alreadyOff);
|
||||
|
||||
const customAttribution = { attribution: { commit: 'Signed-off-by: Someone <a@b.c>' } };
|
||||
assert.strictEqual(withCommitAttributionDisabled(customAttribution), customAttribution);
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
@@ -405,6 +405,33 @@ test('setup preserves an explicit includeCoAuthoredBy opt-in', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('setup preserves an explicit attribution opt-in', () => {
|
||||
withFixture({
|
||||
plugins: [installedPlugin('user')],
|
||||
marketplaces: [officialMarketplace('user')],
|
||||
}, fixture => {
|
||||
// `attribution` wins over `includeCoAuthoredBy` in Claude Code, so ECC must not
|
||||
// add a deprecated key that would silently lose to the user's own setting.
|
||||
fs.writeFileSync(fixture.settingsPath, `${JSON.stringify({
|
||||
attribution: { commit: 'Signed-off-by: Someone <someone@example.com>' },
|
||||
pluginConfigs: {
|
||||
'ecc@ecc': {
|
||||
options: {
|
||||
hooks_enabled: true,
|
||||
hook_profile: 'minimal',
|
||||
},
|
||||
},
|
||||
},
|
||||
}, null, 2)}\n`);
|
||||
|
||||
setupClaudePlugin(setupOptions(fixture, { hooks: 'strict' }));
|
||||
const settings = JSON.parse(fs.readFileSync(fixture.settingsPath, 'utf8'));
|
||||
assert.strictEqual(settings.includeCoAuthoredBy, undefined);
|
||||
assert.deepStrictEqual(settings.attribution, { commit: 'Signed-off-by: Someone <someone@example.com>' });
|
||||
assert.strictEqual(settings.pluginConfigs['ecc@ecc'].options.hook_profile, 'strict');
|
||||
});
|
||||
});
|
||||
|
||||
test('malformed user settings fail preflight without provider mutation or corruption', () => {
|
||||
withFixture({}, fixture => {
|
||||
const malformed = '{"theme":';
|
||||
|
||||
@@ -936,6 +936,33 @@ function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('reinstall preserves an explicit attribution opt-in', () => {
|
||||
const homeDir = createTempDir('install-apply-home-');
|
||||
const projectDir = createTempDir('install-apply-project-');
|
||||
|
||||
try {
|
||||
const claudeRoot = path.join(homeDir, '.claude');
|
||||
fs.mkdirSync(claudeRoot, { recursive: true });
|
||||
const settingsPath = path.join(claudeRoot, 'settings.json');
|
||||
// `attribution` supersedes `includeCoAuthoredBy` in Claude Code, so writing
|
||||
// the deprecated key here would be dead config that loses to the user's choice.
|
||||
const customSettings = {
|
||||
attribution: { commit: 'Signed-off-by: Someone <someone@example.com>' },
|
||||
theme: 'dark',
|
||||
};
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(customSettings, null, 2));
|
||||
|
||||
const install = run(['--profile', 'core'], { cwd: projectDir, homeDir });
|
||||
assert.strictEqual(install.code, 0, install.stderr);
|
||||
|
||||
const afterInstall = readJson(settingsPath);
|
||||
assert.deepStrictEqual(afterInstall, customSettings);
|
||||
} finally {
|
||||
cleanup(homeDir);
|
||||
cleanup(projectDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('ignores malformed existing settings.json during claude install', () => {
|
||||
const homeDir = createTempDir('install-apply-home-');
|
||||
const projectDir = createTempDir('install-apply-project-');
|
||||
|
||||
Reference in New Issue
Block a user