From 321564b62ca925f924c49602a9a539ce74992556 Mon Sep 17 00:00:00 2001 From: Lucas Santana Date: Fri, 3 Jul 2026 11:55:29 -0300 Subject: [PATCH 1/2] feat(security): gate executable dotfiles in shared/ behind --include-dotfiles (#145) + warn on empty profiles (#151) --- src/commands.ts | 106 ++++++++++++++++--- src/index.ts | 6 ++ src/plan.ts | 74 ++++++++++--- src/sharekit.ts | 2 +- test/dotfiles-and-empty.test.ts | 180 ++++++++++++++++++++++++++++++++ 5 files changed, 339 insertions(+), 29 deletions(-) create mode 100644 test/dotfiles-and-empty.test.ts diff --git a/src/commands.ts b/src/commands.ts index 0fb9b79..18df696 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -20,6 +20,7 @@ import { walk, tildify, Dirs, DEFAULT_DIRS, cp, rootsFor } from './paths.js'; export interface InstallOpts { includeHooks?: boolean; + includeDotfiles?: boolean; yes?: boolean; dryRun?: boolean; additive?: boolean; @@ -105,7 +106,8 @@ export function updateApply( user: string, includeHooks = false, dirs: Dirs = DEFAULT_DIRS, - additive = false + additive = false, + includeDotfiles = false ): { filesWritten: number; backupDir: string } { // Get the install record for this user const installed = readInstalled(dirs); @@ -132,10 +134,12 @@ export function updateApply( // Compute roots relative to injected home for testability const roots = rootsFor(dirs.home); const files = plan(dir, roots); - printPlan(files, manifest); + printPlan(files, manifest, includeDotfiles); const todo = files.filter( - (f) => (additive ? f.status === 'new' : f.status !== 'same') && !isExecutable(f, includeHooks) + (f) => + (additive ? f.status === 'new' : f.status !== 'same') && + !isExecutable(f, includeHooks, includeDotfiles) ); if (!todo.length) { console.log(kleur.dim(additive ? '\n No new files to add.\n' : '\n Already up to date.\n')); @@ -143,7 +147,9 @@ export function updateApply( } if (additive) { - const skipped = files.filter((f) => f.status === 'changed' && !isExecutable(f, includeHooks)); + const skipped = files.filter( + (f) => f.status === 'changed' && !isExecutable(f, includeHooks, includeDotfiles) + ); if (skipped.length) { console.log( kleur.dim(`\n = ${skipped.length} locally-modified file(s) preserved (additive mode)`) @@ -151,7 +157,14 @@ export function updateApply( } } - const { backupDir, filesWritten } = applyProfile(todo, user, includeHooks, dirs); + const { backupDir, filesWritten } = applyProfile( + todo, + user, + includeHooks, + dirs, + false, + includeDotfiles + ); // Update the install record with the new commit and timestamp recordInstall(user, dir, ref, manifest.version, dirs); @@ -169,6 +182,7 @@ export async function update( acquireLock(lockPath); try { const includeHooks = opts?.includeHooks ?? false; + const includeDotfiles = opts?.includeDotfiles ?? false; const yes = opts?.yes ?? false; const dryRun = opts?.dryRun ?? false; const additive = opts?.additive ?? false; @@ -198,10 +212,12 @@ export async function update( // Compute roots relative to injected home for testability const roots = rootsFor(dirs.home); const files = plan(fetchDir, roots); - printPlan(files, manifest); + printPlan(files, manifest, includeDotfiles); const todo = files.filter( - (f) => (additive ? f.status === 'new' : f.status !== 'same') && !isExecutable(f, includeHooks) + (f) => + (additive ? f.status === 'new' : f.status !== 'same') && + !isExecutable(f, includeHooks, includeDotfiles) ); if (!todo.length) return void console.log( @@ -209,7 +225,9 @@ export async function update( ); if (additive) { - const skipped = files.filter((f) => f.status === 'changed' && !isExecutable(f, includeHooks)); + const skipped = files.filter( + (f) => f.status === 'changed' && !isExecutable(f, includeHooks, includeDotfiles) + ); if (skipped.length) { console.log( kleur.dim(`\n = ${skipped.length} locally-modified file(s) preserved (additive mode)`) @@ -218,7 +236,7 @@ export async function update( } // If hooks present and not explicitly included, warn - const hasHooks = files.some((f) => isExecutable(f, false)); + const hasHooks = files.some((f) => isExecutable(f, false, true)); if (hasHooks && !includeHooks) { console.log( kleur.yellow(`\n ⚠ This profile's settings.json contains hooks that run shell commands.`) @@ -237,6 +255,28 @@ export async function update( } } + // If dangerous dotfiles present and not explicitly included, warn + const hasDotfiles = files.some((f) => isExecutable(f, true, false)); + if (hasDotfiles && !includeDotfiles) { + console.log( + kleur.yellow( + `\n ⚠ This profile contains executable dotfiles (.zshrc, .bashrc, etc.) that run on shell startup.` + ) + ); + } + + // If dotfiles present and user wants to include them, ask for explicit confirm + if (hasDotfiles && includeDotfiles) { + if ( + !(await confirm( + `This profile contains executable dotfiles that run on shell startup. Update with them?`, + yes + )) + ) { + return void console.log(kleur.dim('\n Aborted.\n')); + } + } + if (!(await confirm(`Apply ${todo.length} change(s)?`, yes))) return void console.log(kleur.dim('\n Aborted.\n')); @@ -249,7 +289,13 @@ export async function update( return; } - const { backupDir, filesWritten } = updateApply(user, includeHooks, dirs, additive); + const { backupDir, filesWritten } = updateApply( + user, + includeHooks, + dirs, + additive, + includeDotfiles + ); console.log( kleur.green(`\n ✓ Updated ${filesWritten} file(s).`) + @@ -266,6 +312,7 @@ export async function install(user: string, opts?: InstallOpts): Promise { acquireLock(lockPath); try { const includeHooks = opts?.includeHooks ?? false; + const includeDotfiles = opts?.includeDotfiles ?? false; const { user: userName, ref: userRef } = parseUserRef(user); const yes = opts?.yes ?? false; const dryRun = opts?.dryRun ?? false; @@ -274,13 +321,21 @@ export async function install(user: string, opts?: InstallOpts): Promise { const manifest = readManifest(dir); const files = plan(dir); console.log(); - printPlan(files, manifest); + printPlan(files, manifest, includeDotfiles); - const todo = files.filter((f) => f.status !== 'same' && !isExecutable(f, includeHooks)); + // Issue #151: Warn if profile has no tool directories (nothing to install) + if (files.length === 0) { + console.log(kleur.yellow(`\n ⚠ profile has no tool directories — nothing to install\n`)); + process.exit(1); + } + + const todo = files.filter( + (f) => f.status !== 'same' && !isExecutable(f, includeHooks, includeDotfiles) + ); if (!todo.length) return void console.log(kleur.dim('\n Already up to date.\n')); // If hooks present and not explicitly included, warn - const hasHooks = files.some((f) => isExecutable(f, false)); + const hasHooks = files.some((f) => isExecutable(f, false, true)); if (hasHooks && !includeHooks) { console.log( kleur.yellow(`\n ⚠ This profile's settings.json contains hooks that run shell commands.`) @@ -299,6 +354,28 @@ export async function install(user: string, opts?: InstallOpts): Promise { } } + // If dangerous dotfiles present and not explicitly included, warn + const hasDotfiles = files.some((f) => isExecutable(f, true, false)); + if (hasDotfiles && !includeDotfiles) { + console.log( + kleur.yellow( + `\n ⚠ This profile contains executable dotfiles (.zshrc, .bashrc, etc.) that run on shell startup.` + ) + ); + } + + // If dotfiles present and user wants to include them, ask for explicit confirm + if (hasDotfiles && includeDotfiles) { + if ( + !(await confirm( + `This profile contains executable dotfiles that run on shell startup. Install them?`, + yes + )) + ) { + return void console.log(kleur.dim('\n Aborted.\n')); + } + } + if (!(await confirm(`Apply ${todo.length} change(s)?`, yes))) return void console.log(kleur.dim('\n Aborted.\n')); @@ -307,7 +384,8 @@ export async function install(user: string, opts?: InstallOpts): Promise { userName, includeHooks, DEFAULT_DIRS, - dryRun + dryRun, + includeDotfiles ); // Only record install if not a dry-run diff --git a/src/index.ts b/src/index.ts index 3bd8949..8853bf4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,6 +33,7 @@ const USAGE = `${kleur.bold('sharekit')} v${VERSION} — share your AI coding se --force exit 0 even if high-severity findings detected ${kleur.cyan('install')} [@] [opts] fetch, preview, apply a profile --include-hooks also install settings.json with shell hooks + --include-dotfiles also install executable shared/ dotfiles --yes auto-approve prompts --dry-run show changes without writing files ${kleur.cyan('preview')} [@] show changes, apply nothing @@ -42,6 +43,7 @@ const USAGE = `${kleur.bold('sharekit')} v${VERSION} — share your AI coding se --yes auto-approve prompts --dry-run show changes without writing files --include-hooks also apply settings.json with shell hooks + --include-dotfiles also apply executable shared/ dotfiles --additive only add new files; preserve local edits ${kleur.cyan('rollback')} [opts] restore the last backup --yes auto-approve prompts @@ -106,6 +108,7 @@ export async function main(argv = process.argv.slice(2)) { if (flags.includes('--yes')) opts.yes = true; if (flags.includes('--dry-run')) opts.dryRun = true; if (flags.includes('--include-hooks')) opts.includeHooks = true; + if (flags.includes('--include-dotfiles')) opts.includeDotfiles = true; if (flags.includes('--additive')) opts.additive = true; await update(user, opts); return; @@ -288,6 +291,9 @@ export async function main(argv = process.argv.slice(2)) { if (cmd === 'install' && flags.includes('--include-hooks')) { opts.includeHooks = true; } + if (cmd === 'install' && flags.includes('--include-dotfiles')) { + opts.includeDotfiles = true; + } await fn(arg, opts); } diff --git a/src/plan.ts b/src/plan.ts index 8ebf379..46f6053 100644 --- a/src/plan.ts +++ b/src/plan.ts @@ -15,6 +15,22 @@ export interface PlanFile { status: Status; } +// Denylist of executable-on-load dotfiles in shared/ that bypass shell gate +// These files are sourced/executed on every shell startup and pose an RCE risk +export const DANGEROUS_SHARED_DOTFILES = new Set([ + '.zshrc', + '.zshenv', + '.zprofile', + '.zlogin', + '.bashrc', + '.bash_profile', + '.bash_login', + '.profile', + '.bash_logout', + '.xinitrc', + '.xprofile', +]); + // Track skipped symlinks for the current plan let currentPlanSkippedSymlinks: string[] = []; @@ -57,10 +73,27 @@ function classify(src: string, dest: string): Status { // ponytail: settings.json carries hooks (arbitrary shell). v1 never auto-installs it. // add `--include-hooks` when someone actually asks. -export const isExecutable = (f: PlanFile, includeHooks = false) => - !includeHooks && f.tool === 'claude' && path.basename(f.dest) === 'settings.json'; +// shared/ dotfiles (.zshrc, .bashrc, etc.) are sourced on shell startup → RCE on install. +// add `--include-dotfiles` when someone explicitly requests. +export const isExecutable = (f: PlanFile, includeHooks = false, includeDotfiles = false) => { + if (!includeHooks && f.tool === 'claude' && path.basename(f.dest) === 'settings.json') { + return true; + } + if ( + !includeDotfiles && + f.tool === 'shared' && + DANGEROUS_SHARED_DOTFILES.has(path.basename(f.dest)) + ) { + return true; + } + return false; +}; -export function printPlan(files: PlanFile[], manifest: ReturnType): void { +export function printPlan( + files: PlanFile[], + manifest: ReturnType, + includeDotfiles = false +): void { console.log( kleur.bold(`Profile: ${manifest.name}${manifest.version ? ' v' + manifest.version : ''}`) ); @@ -75,16 +108,22 @@ export function printPlan(files: PlanFile[], manifest: ReturnType f.status === 'same').length; if (same) console.log(kleur.dim(`\n = ${same} unchanged`)); - if (files.some((f) => isExecutable(f))) + if (files.some((f) => isExecutable(f, false, includeDotfiles))) console.log( kleur.yellow(`\n ⚠ settings.json present — contains hooks; skipped. Merge manually.`) ); + if (files.some((f) => isExecutable(f, true, false) && f.tool === 'shared')) + console.log( + kleur.yellow( + `\n ⚠ shared/ contains executable dotfiles (.zshrc, .bashrc, etc.); skipped for security. Use --include-dotfiles to merge.` + ) + ); } -function write(files: PlanFile[], includeHooks = false): number { +function write(files: PlanFile[], includeHooks = false, includeDotfiles = false): number { let n = 0; for (const f of files) { - if (f.status === 'same' || isExecutable(f, includeHooks)) continue; + if (f.status === 'same' || isExecutable(f, includeHooks, includeDotfiles)) continue; fs.mkdirSync(path.dirname(f.dest), { recursive: true }); cp(f.src, f.dest); n++; @@ -97,10 +136,13 @@ export function writeAtomic( backupDir: string, user: string, includeHooks = false, - dirs: Dirs = DEFAULT_DIRS + dirs: Dirs = DEFAULT_DIRS, + includeDotfiles = false ): number { let n = 0; - const applied = files.filter((f) => f.status !== 'same' && !isExecutable(f, includeHooks)); + const applied = files.filter( + (f) => f.status !== 'same' && !isExecutable(f, includeHooks, includeDotfiles) + ); try { for (const f of applied) { @@ -127,11 +169,14 @@ export function backup( files: PlanFile[], user: string, includeHooks = false, - dirs: Dirs = DEFAULT_DIRS + dirs: Dirs = DEFAULT_DIRS, + includeDotfiles = false ): string { const stamp = new Date().toISOString().replace(/[:.]/g, '-'); const dir = path.join(dirs.state, 'backups', `${user}-${stamp}`); - const applied = files.filter((f) => f.status !== 'same' && !isExecutable(f, includeHooks)); + const applied = files.filter( + (f) => f.status !== 'same' && !isExecutable(f, includeHooks, includeDotfiles) + ); fs.mkdirSync(dir, { recursive: true }); for (const f of applied.filter((f) => f.status === 'changed')) { const t = path.join(dir, path.relative(dirs.home, f.dest)); @@ -168,17 +213,18 @@ export function applyProfile( user: string, includeHooks = false, dirs: Dirs = DEFAULT_DIRS, - dryRun = false + dryRun = false, + includeDotfiles = false ): { backupDir: string; filesWritten: number } { if (dryRun) { // In dry-run, just count files without writing anything const filesWritten = files.filter( - (f) => f.status !== 'same' && !isExecutable(f, includeHooks) + (f) => f.status !== 'same' && !isExecutable(f, includeHooks, includeDotfiles) ).length; return { backupDir: '', filesWritten }; } - const backupDir = backup(files, user, includeHooks, dirs); - const filesWritten = writeAtomic(files, backupDir, user, includeHooks, dirs); + const backupDir = backup(files, user, includeHooks, dirs, includeDotfiles); + const filesWritten = writeAtomic(files, backupDir, user, includeHooks, dirs, includeDotfiles); pruneBackups(user, dirs.state); return { backupDir, filesWritten }; } diff --git a/src/sharekit.ts b/src/sharekit.ts index c81c3ac..9ef644d 100644 --- a/src/sharekit.ts +++ b/src/sharekit.ts @@ -22,7 +22,7 @@ export type { Finding } from './scanner.js'; export { parseUserRef, fetchProfile, readManifest } from './fetch.js'; // From plan.ts -export { plan, printPlan, isExecutable, applyProfile } from './plan.js'; +export { plan, printPlan, isExecutable, applyProfile, DANGEROUS_SHARED_DOTFILES } from './plan.js'; export type { Status, PlanFile } from './plan.js'; // From backup.ts diff --git a/test/dotfiles-and-empty.test.ts b/test/dotfiles-and-empty.test.ts new file mode 100644 index 0000000..54e4502 --- /dev/null +++ b/test/dotfiles-and-empty.test.ts @@ -0,0 +1,180 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { plan, applyProfile, DANGEROUS_SHARED_DOTFILES, isExecutable } from '../src/sharekit.ts'; + +test('dotfiles: .zshrc and similar files are gated in shared/ by default', () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-dotfiles-')); + const profile = path.join(tmp, 'profile'); + const home = path.join(tmp, 'home'); + const state = path.join(tmp, 'state'); + + // Create profile with dangerous dotfiles in shared/ + fs.mkdirSync(path.join(profile, 'shared'), { recursive: true }); + fs.writeFileSync(path.join(profile, 'shared', '.zshrc'), 'evil zsh config'); + fs.writeFileSync(path.join(profile, 'shared', '.bashrc'), 'evil bash config'); + fs.writeFileSync(path.join(profile, 'shared', 'normal-file.txt'), 'safe file'); + + const roots = { + claude: path.join(home, '.claude'), + cursor: path.join(home, '.cursor'), + opencode: path.join(home, '.config', 'opencode'), + gjc: path.join(home, '.gjc'), + shared: home, + }; + const dirs = { home, state }; + + const files = plan(profile, roots); + + // .zshrc and .bashrc should be marked as executable (gated) + const zshrc = files.find((f) => f.rel === '.zshrc'); + const bashrc = files.find((f) => f.rel === '.bashrc'); + const normalFile = files.find((f) => f.rel === 'normal-file.txt'); + + assert.ok(zshrc, '.zshrc should be in plan'); + assert.ok(bashrc, '.bashrc should be in plan'); + assert.ok(normalFile, 'normal-file.txt should be in plan'); + + // Test isExecutable: by default includeDotfiles=false, so dotfiles are gated + assert.ok(isExecutable(zshrc!, false, false), '.zshrc should be gated by default'); + assert.ok(isExecutable(bashrc!, false, false), '.bashrc should be gated by default'); + assert.ok(!isExecutable(normalFile!, false, false), 'normal-file.txt should NOT be gated'); + + // Test applyProfile with includeDotfiles=false (default) + const { filesWritten: filesWritten1 } = applyProfile( + files, + 'testuser', + false, + dirs, + false, + false + ); + + // Only normal-file.txt should be written (not .zshrc, .bashrc) + assert.equal(filesWritten1, 1, 'should write only normal-file.txt'); + assert.ok( + fs.existsSync(path.join(home, 'normal-file.txt')), + 'normal-file.txt should be installed' + ); + assert.ok(!fs.existsSync(path.join(home, '.zshrc')), '.zshrc should NOT be installed'); + assert.ok(!fs.existsSync(path.join(home, '.bashrc')), '.bashrc should NOT be installed'); + + fs.rmSync(tmp, { recursive: true }); +}); + +test('dotfiles: with includeDotfiles=true, dangerous files ARE installed', () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-dotfiles-include-')); + const profile = path.join(tmp, 'profile'); + const home = path.join(tmp, 'home'); + const state = path.join(tmp, 'state'); + + // Create profile with dangerous dotfiles in shared/ + fs.mkdirSync(path.join(profile, 'shared'), { recursive: true }); + fs.writeFileSync(path.join(profile, 'shared', '.zshrc'), 'zsh config'); + fs.writeFileSync(path.join(profile, 'shared', 'normal-file.txt'), 'safe file'); + + const roots = { + claude: path.join(home, '.claude'), + cursor: path.join(home, '.cursor'), + opencode: path.join(home, '.config', 'opencode'), + gjc: path.join(home, '.gjc'), + shared: home, + }; + const dirs = { home, state }; + + const files = plan(profile, roots); + + // With includeDotfiles=true, isExecutable should return false for dotfiles + const zshrc = files.find((f) => f.rel === '.zshrc'); + assert.ok( + !isExecutable(zshrc!, false, true), + '.zshrc should NOT be gated when includeDotfiles=true' + ); + + // Apply with includeDotfiles=true + const { filesWritten } = applyProfile(files, 'testuser', false, dirs, false, true); + + // Both files should be written + assert.equal(filesWritten, 2, 'should write both .zshrc and normal-file.txt'); + assert.ok( + fs.existsSync(path.join(home, 'normal-file.txt')), + 'normal-file.txt should be installed' + ); + assert.ok( + fs.existsSync(path.join(home, '.zshrc')), + '.zshrc SHOULD be installed with includeDotfiles=true' + ); + + fs.rmSync(tmp, { recursive: true }); +}); + +test('dotfiles: claude/settings.json gating unchanged', () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-settings-')); + const profile = path.join(tmp, 'profile'); + const home = path.join(tmp, 'home'); + const state = path.join(tmp, 'state'); + + // Create profile with settings.json in claude/ + fs.mkdirSync(path.join(profile, 'claude'), { recursive: true }); + fs.writeFileSync(path.join(profile, 'claude', 'settings.json'), '{"hooks": []}'); + + const roots = { + claude: path.join(home, '.claude'), + cursor: path.join(home, '.cursor'), + opencode: path.join(home, '.config', 'opencode'), + gjc: path.join(home, '.gjc'), + shared: home, + }; + const dirs = { home, state }; + + const files = plan(profile, roots); + const settings = files.find((f) => f.rel === 'settings.json'); + + assert.ok(settings, 'settings.json should be in plan'); + + // settings.json should still be gated by includeHooks, not includeDotfiles + assert.ok(isExecutable(settings!, false, false), 'settings.json should be gated by includeHooks'); + assert.ok( + isExecutable(settings!, false, true), + 'settings.json should be gated even with includeDotfiles=true' + ); + assert.ok( + !isExecutable(settings!, true, false), + 'settings.json should NOT be gated with includeHooks=true' + ); + + fs.rmSync(tmp, { recursive: true }); +}); + +test('empty profile: install should fail when no files are present (#151)', () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-empty-')); + const profile = path.join(tmp, 'profile'); + + // Create profile with no tool directories + fs.mkdirSync(profile, { recursive: true }); + fs.writeFileSync(path.join(profile, 'sharekit.toml'), '[profile]\nname = "empty"\n'); + + const roots = { + claude: path.join(tmp, 'home', '.claude'), + cursor: path.join(tmp, 'home', '.cursor'), + opencode: path.join(tmp, 'home', '.config', 'opencode'), + gjc: path.join(tmp, 'home', '.gjc'), + shared: path.join(tmp, 'home'), + }; + + const files = plan(profile, roots); + assert.equal(files.length, 0, 'plan should have zero files for empty profile'); + + fs.rmSync(tmp, { recursive: true }); +}); + +test('DANGEROUS_SHARED_DOTFILES denylist is exported and contains expected files', () => { + assert.ok(DANGEROUS_SHARED_DOTFILES instanceof Set, 'DANGEROUS_SHARED_DOTFILES should be a Set'); + assert.ok(DANGEROUS_SHARED_DOTFILES.has('.zshrc'), '.zshrc should be in denylist'); + assert.ok(DANGEROUS_SHARED_DOTFILES.has('.bashrc'), '.bashrc should be in denylist'); + assert.ok(DANGEROUS_SHARED_DOTFILES.has('.zshenv'), '.zshenv should be in denylist'); + assert.ok(DANGEROUS_SHARED_DOTFILES.has('.profile'), '.profile should be in denylist'); + assert.ok(DANGEROUS_SHARED_DOTFILES.has('.xinitrc'), '.xinitrc should be in denylist'); +}); From ce4cfd5746f9b155735762c0b9d3120817300e88 Mon Sep 17 00:00:00 2001 From: Lucas Santana Date: Fri, 3 Jul 2026 11:58:46 -0300 Subject: [PATCH 2/2] docs: add dotfile gating to README Hooks & Safety and SECURITY trust model --- README.md | 18 +++++++++++++++++- SECURITY.md | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 360b098..b08d628 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,11 @@ Update only works on HEAD-tracked profiles. Pinned refs are skipped. ### Hooks & Safety -Settings files (`.claude/settings.json`) that define hooks are **flagged but not installed by default**—hooks run shell commands and require explicit trust. After reviewing a profile in `preview`, install hooks with: +Sharekit gates two types of executable files by default: hooks in `.claude/settings.json` and shell startup files in `shared/`. Both are **flagged but not installed by default** because they run commands under your account and require explicit trust. + +#### Hooks in `settings.json` + +Settings files (`.claude/settings.json`) may define hooks that run shell commands. After reviewing a profile in `preview`, install hooks with: ```bash npx @lucassantana/sharekit install --include-hooks @@ -85,6 +89,18 @@ npx @lucassantana/sharekit install --include-hooks You'll get a second confirmation before the settings file is written. +#### Executable dotfiles in `shared/` + +Shell startup files in `shared/` like `.zshrc`, `.bashrc`, `.profile`, `.xinitrc` and similar files are **automatically skipped for security** — these are sourced on every shell startup and could execute arbitrary code when you open a terminal. Review the profile in `preview`, then optionally include them: + +```bash +npx @lucassantana/sharekit install --include-dotfiles +``` + +You'll get a second confirmation before dotfiles are written. + +Both flags also work with `update` and are independent—you can install hooks without dotfiles, dotfiles without hooks, or both. + --- ## Discover Profiles diff --git a/SECURITY.md b/SECURITY.md index eb1ae3e..dcfd2e7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -17,6 +17,7 @@ This means: **What sharekit enforces:** - Files are only written inside `~/.claude/` (or `~/.cursor/`, `~/` for shared files) — path escapes are rejected - `settings.json` (hooks) is **never installed by default**; pass `--include-hooks` to opt in after reviewing +- Executable dotfiles in `shared/` (`.zshrc`, `.zshenv`, `.zprofile`, `.zlogin`, `.bashrc`, `.bash_profile`, `.bash_login`, `.profile`, `.bash_logout`, `.xinitrc`, `.xprofile`) are **never installed by default**; pass `--include-dotfiles` to opt in after reviewing. These files run on every shell startup and pose an RCE risk. - Secret scanning (`sharekit scan`) blocks profiles containing high-confidence secrets **`applied.json` trust note:** The backup metadata stored in `~/.sharekit/backups/` is generated by sharekit during install and is trusted during restore/uninstall operations. Tampering with `applied.json` is rate-limited to the file paths within your home directory.