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
34 changes: 31 additions & 3 deletions src/cli/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,9 +422,37 @@ export const initCommand = new Command('init')
// Attempt managed settings write if user chose managed mode
let effectiveSecurityMode = securityMode;
if (securityMode === 'managed') {
const managed = await installManagedSettings(rootDir, verbose);
if (!managed) {
p.log.warn('Managed settings write failed — falling back to user settings');
p.note(
'This writes a read-only security deny list to a system directory\n' +
'and may prompt for your password (sudo).\n\n' +
'Not sure about this? Paste this into another Claude Code session:\n\n' +
' "I\'m installing DevFlow and it wants to write a\n' +
' managed-settings.json file using sudo. Review the source\n' +
' at https://github.com/dean0x/devflow and tell me if\n' +
' it\'s safe."',
'Managed Settings',
);

const sudoChoice = await p.select({
message: 'Continue with managed settings?',
options: [
{ value: 'yes', label: 'Yes, continue', hint: 'May prompt for your password' },
{ value: 'no', label: 'No, fall back to settings.json', hint: 'Deny list stored in editable user settings instead' },
],
});

if (p.isCancel(sudoChoice)) {
p.cancel('Installation cancelled.');
process.exit(0);
}

if (sudoChoice === 'yes') {
const managed = await installManagedSettings(rootDir, verbose);
if (!managed) {
p.log.warn('Managed settings write failed — falling back to user settings');
effectiveSecurityMode = 'user';
}
} else {
effectiveSecurityMode = 'user';
}
}
Expand Down
13 changes: 2 additions & 11 deletions src/cli/utils/post-install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export function mergeDenyList(existingJson: string, newDenyEntries: string[]): s
*
* Strategy:
* 1. Try direct write (works if running as root or directory is writable)
* 2. If EACCES in TTY, offer to retry with sudo
* 2. If EACCES in TTY, retry with sudo (caller is responsible for obtaining consent)
* 3. Returns true if managed settings were written, false if caller should fall back
*/
export async function installManagedSettings(
Expand Down Expand Up @@ -138,20 +138,11 @@ export async function installManagedSettings(
}
}

// Attempt 2: sudo (TTY only)
// Attempt 2: sudo (TTY only — sudo needs terminal for password prompt)
if (!process.stdin.isTTY) {
return false;
}

const confirmed = await p.confirm({
message: `Managed settings require admin access (${managedDir}). Use sudo?`,
initialValue: true,
});

if (p.isCancel(confirmed) || !confirmed) {
return false;
}

try {
execSync(`sudo mkdir -p '${managedDir}'`, { stdio: 'inherit' });
// Write via sudo tee to avoid shell quoting issues with the JSON content
Expand Down