Skip to content
Merged
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
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,30 @@ 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 <github-user> --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 <github-user> --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
Expand Down
1 change: 1 addition & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
106 changes: 92 additions & 14 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -132,26 +134,37 @@ 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'));
return { filesWritten: 0, backupDir: '' };
}

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)`)
);
}
}

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);
Expand All @@ -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;
Expand Down Expand Up @@ -198,18 +212,22 @@ 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(
kleur.dim(additive ? '\n No new files to add.\n' : '\n Already up to date.\n')
);

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)`)
Expand All @@ -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.`)
Expand All @@ -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'));

Expand All @@ -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).`) +
Expand All @@ -266,6 +312,7 @@ export async function install(user: string, opts?: InstallOpts): Promise<void> {
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;
Expand All @@ -274,13 +321,21 @@ export async function install(user: string, opts?: InstallOpts): Promise<void> {
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.`)
Expand All @@ -299,6 +354,28 @@ export async function install(user: string, opts?: InstallOpts): Promise<void> {
}
}

// 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'));

Expand All @@ -307,7 +384,8 @@ export async function install(user: string, opts?: InstallOpts): Promise<void> {
userName,
includeHooks,
DEFAULT_DIRS,
dryRun
dryRun,
includeDotfiles
);

// Only record install if not a dry-run
Expand Down
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')} <user>[@<ref>] [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')} <user>[@<ref>] show changes, apply nothing
Expand All @@ -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')} <user> [opts] restore the last backup
--yes auto-approve prompts
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
Loading
Loading