Skip to content

Instantly share code, notes, and snippets.

@ideabrian
Last active January 7, 2026 03:29
Show Gist options
  • Select an option

  • Save ideabrian/0f9a0406f83ed5968cfbd94047fcf9b7 to your computer and use it in GitHub Desktop.

Select an option

Save ideabrian/0f9a0406f83ed5968cfbd94047fcf9b7 to your computer and use it in GitHub Desktop.
Claude Code Settings UI - VS Code-style editor for settings.json and CLAUDE.md

Claude Code Settings UI

VS Code-style local editor for ~/.claude/settings.json and ~/.claude/CLAUDE.md

Features

  • Dual Mode Editor - Toggle between UI form and raw JSON (like VS Code settings)
  • Monaco Editor - Full VS Code editor for JSON and Markdown with syntax highlighting
  • Live Preview - Side-by-side Markdown preview for CLAUDE.md
  • Permission Presets - One-click load Boris's safe defaults, minimal, or YOLO mode
  • Auto-save - Changes persist automatically with 1s debounce

Quick Start

gh gist clone 0f9a0406f83ed5968cfbd94047fcf9b7 claude-settings-ui
cd claude-settings-ui
mkdir -p static && mv index.html static/
npm install
npm start
# Open http://localhost:3456

Permission Presets

Preset Allow Ask Deny Description
Boris's Safe Defaults 43 3 7 Safe git, npm, file ops (based on 19k tool uses)
Minimal Safe 8 7 3 Read-only bias, cautious
YOLO Mode 17 1 2 Trust Claude, fast iteration

Settings Supported

  • Model - Default model selector (Sonnet 4, Opus 4, Opus 4.5)
  • Always Thinking - Enable extended thinking by default
  • Status Line - Custom shell command for status bar
  • Hooks - SessionStart, SessionEnd, PreToolUse, PostToolUse
  • Permissions - Allow/Ask/Deny rules with Bash(command:*) pattern

Tech Stack

  • Hono - Fast web framework
  • Monaco Editor - VS Code's editor
  • Vanilla JS - No build step required

Credits

Permission presets inspired by Boris Cherny's config (Claude Code creator)


Made with Claude Code

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Claude Code Settings</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/editor/editor.main.css">
<style>
:root {
--bg-primary: #1e1e1e;
--bg-secondary: #252526;
--bg-tertiary: #2d2d2d;
--text-primary: #cccccc;
--text-secondary: #969696;
--accent: #3b82f6;
--accent-hover: #2563eb;
--border: #3c3c3c;
--success: #10b981;
--error: #ef4444;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
height: 100vh;
overflow: hidden;
}
.container {
display: flex;
flex-direction: column;
height: 100vh;
}
/* Header */
header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
}
.logo {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
font-size: 14px;
}
.logo svg { width: 24px; height: 24px; }
/* Tabs */
.tabs {
display: flex;
gap: 2px;
background: var(--bg-primary);
}
.tab {
padding: 10px 20px;
background: var(--bg-tertiary);
border: none;
color: var(--text-secondary);
cursor: pointer;
font-size: 13px;
transition: all 0.15s;
}
.tab:hover { color: var(--text-primary); }
.tab.active {
background: var(--bg-secondary);
color: var(--text-primary);
border-top: 2px solid var(--accent);
}
/* Mode toggle */
.mode-toggle {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--text-secondary);
}
.toggle-btn {
padding: 6px 12px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-secondary);
cursor: pointer;
font-size: 12px;
transition: all 0.15s;
}
.toggle-btn:hover { border-color: var(--accent); color: var(--text-primary); }
.toggle-btn.active { background: var(--accent); border-color: var(--accent); color: white; }
/* Main content */
main {
flex: 1;
display: flex;
overflow: hidden;
}
.panel {
flex: 1;
display: none;
overflow: hidden;
}
.panel.active { display: flex; flex-direction: column; }
/* Settings form mode */
.settings-form {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.setting-group {
margin-bottom: 24px;
padding-bottom: 24px;
border-bottom: 1px solid var(--border);
}
.setting-group:last-child { border-bottom: none; }
.setting-group h3 {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-secondary);
margin-bottom: 16px;
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
gap: 20px;
}
.setting-info { flex: 1; }
.setting-label {
font-size: 13px;
color: var(--text-primary);
margin-bottom: 4px;
}
.setting-description {
font-size: 12px;
color: var(--text-secondary);
line-height: 1.4;
}
.setting-control { min-width: 200px; }
/* Form controls */
input[type="text"], select {
width: 100%;
padding: 8px 12px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-primary);
font-size: 13px;
}
input[type="text"]:focus, select:focus {
outline: none;
border-color: var(--accent);
}
/* Toggle switch */
.toggle-switch {
position: relative;
width: 40px;
height: 20px;
}
.toggle-switch input { display: none; }
.toggle-slider {
position: absolute;
inset: 0;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 20px;
cursor: pointer;
transition: all 0.2s;
}
.toggle-slider::before {
content: '';
position: absolute;
width: 14px;
height: 14px;
left: 2px;
top: 2px;
background: var(--text-secondary);
border-radius: 50%;
transition: all 0.2s;
}
.toggle-switch input:checked + .toggle-slider {
background: var(--accent);
border-color: var(--accent);
}
.toggle-switch input:checked + .toggle-slider::before {
transform: translateX(20px);
background: white;
}
/* Editor container */
.editor-container {
flex: 1;
position: relative;
}
#settings-editor, #claude-md-editor {
width: 100%;
height: 100%;
}
/* Markdown preview */
.md-split {
display: flex;
flex: 1;
}
.md-split > div { flex: 1; }
.md-preview {
padding: 20px;
overflow-y: auto;
background: var(--bg-secondary);
border-left: 1px solid var(--border);
}
.md-preview h1, .md-preview h2, .md-preview h3 { margin: 16px 0 8px; }
.md-preview h1 { font-size: 24px; border-bottom: 1px solid var(--border); padding-bottom: 8px; }
.md-preview h2 { font-size: 18px; }
.md-preview h3 { font-size: 14px; }
.md-preview p { margin: 8px 0; line-height: 1.6; }
.md-preview code {
background: var(--bg-tertiary);
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
}
.md-preview pre {
background: var(--bg-tertiary);
padding: 12px;
border-radius: 4px;
overflow-x: auto;
margin: 12px 0;
}
.md-preview pre code { background: none; padding: 0; }
.md-preview ul, .md-preview ol { margin: 8px 0; padding-left: 24px; }
.md-preview li { margin: 4px 0; }
.md-preview blockquote {
border-left: 3px solid var(--accent);
padding-left: 12px;
margin: 12px 0;
color: var(--text-secondary);
}
/* Status bar */
.status-bar {
display: flex;
justify-content: space-between;
padding: 4px 12px;
background: var(--accent);
font-size: 12px;
color: white;
}
.status-bar.error { background: var(--error); }
.status-bar.success { background: var(--success); }
/* Hooks editor */
.hooks-editor {
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 4px;
padding: 12px;
}
.hook-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
padding: 8px;
background: var(--bg-secondary);
border-radius: 4px;
}
.hook-item input {
flex: 1;
}
.hook-type {
font-size: 11px;
padding: 2px 8px;
background: var(--accent);
border-radius: 3px;
color: white;
}
.btn-small {
padding: 4px 8px;
background: transparent;
border: 1px solid var(--border);
border-radius: 3px;
color: var(--text-secondary);
cursor: pointer;
font-size: 11px;
}
.btn-small:hover { border-color: var(--accent); color: var(--text-primary); }
.btn-small.danger:hover { border-color: var(--error); color: var(--error); }
/* Permissions editor */
.perm-section {
margin-bottom: 16px;
}
.perm-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.perm-badge {
font-size: 11px;
padding: 3px 10px;
border-radius: 12px;
font-weight: 500;
}
.perm-badge.allow { background: var(--success); color: white; }
.perm-badge.ask { background: #f59e0b; color: white; }
.perm-badge.deny { background: var(--error); color: white; }
.perm-list {
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 4px;
max-height: 200px;
overflow-y: auto;
}
.perm-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
border-bottom: 1px solid var(--border);
font-family: monospace;
font-size: 12px;
}
.perm-item:last-child { border-bottom: none; }
.perm-item:hover { background: var(--bg-secondary); }
.perm-input {
display: flex;
gap: 8px;
margin-top: 8px;
}
.perm-input input {
flex: 1;
font-family: monospace;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1Z"/>
</svg>
Claude Code Settings
</div>
<div class="tabs">
<button class="tab active" data-panel="settings">settings.json</button>
<button class="tab" data-panel="claude-md">CLAUDE.md</button>
</div>
<div class="mode-toggle" id="settings-mode-toggle">
<button class="toggle-btn active" data-mode="form">UI</button>
<button class="toggle-btn" data-mode="json">JSON</button>
</div>
</header>
<main>
<!-- Settings Panel -->
<div class="panel active" id="settings-panel">
<!-- Form Mode -->
<div class="settings-form" id="settings-form">
<div class="setting-group">
<h3>General</h3>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Model</div>
<div class="setting-description">Default model for Claude Code sessions</div>
</div>
<div class="setting-control">
<select id="setting-model">
<option value="">Not set (use default)</option>
<option value="claude-sonnet-4-20250514">Sonnet 4</option>
<option value="claude-opus-4-20250115">Opus 4</option>
<option value="claude-opus-4-5-20251101">Opus 4.5</option>
</select>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Always Enable Thinking</div>
<div class="setting-description">Enable extended thinking mode by default</div>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" id="setting-thinking">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
<div class="setting-group">
<h3>Status Line</h3>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Command</div>
<div class="setting-description">Shell command to generate status line content</div>
</div>
<div class="setting-control">
<input type="text" id="setting-statusline-cmd" placeholder="e.g., ~/.claude/scripts/statusline.sh">
</div>
</div>
</div>
<div class="setting-group">
<h3>Hooks</h3>
<div id="hooks-container"></div>
</div>
<div class="setting-group">
<h3>Permissions</h3>
<div class="setting-description" style="margin-bottom: 12px;">
Control which commands Claude can run. Format: <code>Bash(command:*)</code>
</div>
<div id="permissions-container"></div>
</div>
</div>
<!-- JSON Mode -->
<div class="editor-container" id="settings-editor-container" style="display: none;">
<div id="settings-editor"></div>
</div>
</div>
<!-- CLAUDE.md Panel -->
<div class="panel" id="claude-md-panel">
<div class="md-split">
<div class="editor-container">
<div id="claude-md-editor"></div>
</div>
<div class="md-preview" id="md-preview"></div>
</div>
</div>
</main>
<div class="status-bar" id="status-bar">
<span id="status-message">Ready</span>
<span id="status-file">~/.claude/settings.json</span>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/marked@9.1.6/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
<script>
let settingsEditor, claudeMdEditor;
let currentSettings = {};
let isDirty = false;
// Monaco setup
require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' } });
require(['vs/editor/editor.main'], async function () {
// Define VS Code dark theme
monaco.editor.defineTheme('claude-dark', {
base: 'vs-dark',
inherit: true,
rules: [],
colors: {
'editor.background': '#1e1e1e',
'editor.foreground': '#cccccc',
}
});
// Settings JSON editor
settingsEditor = monaco.editor.create(document.getElementById('settings-editor'), {
language: 'json',
theme: 'claude-dark',
minimap: { enabled: false },
fontSize: 13,
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
tabSize: 2,
});
// CLAUDE.md editor
claudeMdEditor = monaco.editor.create(document.getElementById('claude-md-editor'), {
language: 'markdown',
theme: 'claude-dark',
minimap: { enabled: false },
fontSize: 13,
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
wordWrap: 'on',
});
// Load data
await loadSettings();
await loadClaudeMd();
// Auto-save on change
settingsEditor.onDidChangeModelContent(debounce(saveSettings, 1000));
claudeMdEditor.onDidChangeModelContent(debounce(saveClaudeMd, 1000));
claudeMdEditor.onDidChangeModelContent(updatePreview);
});
// Tab switching
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
tab.classList.add('active');
document.getElementById(`${tab.dataset.panel}-panel`).classList.add('active');
// Update status bar
const file = tab.dataset.panel === 'settings' ? '~/.claude/settings.json' : '~/.claude/CLAUDE.md';
document.getElementById('status-file').textContent = file;
// Show/hide mode toggle
document.getElementById('settings-mode-toggle').style.display =
tab.dataset.panel === 'settings' ? 'flex' : 'none';
});
});
// Mode toggle (form/json)
document.querySelectorAll('.mode-toggle .toggle-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.mode-toggle .toggle-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const isForm = btn.dataset.mode === 'form';
document.getElementById('settings-form').style.display = isForm ? 'block' : 'none';
document.getElementById('settings-editor-container').style.display = isForm ? 'none' : 'block';
if (!isForm) {
// Sync form to JSON
settingsEditor.setValue(JSON.stringify(currentSettings, null, 2));
} else {
// Sync JSON to form
try {
currentSettings = JSON.parse(settingsEditor.getValue());
populateForm();
} catch (e) {}
}
});
});
async function loadSettings() {
try {
const res = await fetch('/api/settings');
const data = await res.json();
currentSettings = data.parsed;
settingsEditor.setValue(data.content);
populateForm();
setStatus('Loaded settings.json', 'success');
} catch (e) {
setStatus('Error loading settings: ' + e.message, 'error');
}
}
async function saveSettings() {
try {
const content = settingsEditor.getValue();
JSON.parse(content); // Validate
await fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content })
});
setStatus('Saved settings.json', 'success');
} catch (e) {
setStatus('Error: ' + e.message, 'error');
}
}
async function loadClaudeMd() {
try {
const res = await fetch('/api/claude-md');
const data = await res.json();
claudeMdEditor.setValue(data.content);
updatePreview();
setStatus('Loaded CLAUDE.md', 'success');
} catch (e) {
setStatus('Error loading CLAUDE.md: ' + e.message, 'error');
}
}
async function saveClaudeMd() {
try {
const content = claudeMdEditor.getValue();
await fetch('/api/claude-md', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content })
});
setStatus('Saved CLAUDE.md', 'success');
} catch (e) {
setStatus('Error: ' + e.message, 'error');
}
}
function populateForm() {
// Model
document.getElementById('setting-model').value = currentSettings.model || '';
// Thinking
document.getElementById('setting-thinking').checked = currentSettings.alwaysThinkingEnabled || false;
// Status line
document.getElementById('setting-statusline-cmd').value = currentSettings.statusLine?.command || '';
// Hooks
renderHooks();
// Permissions
renderPermissions();
}
// Permission presets (inspired by bcherny's config)
const PERMISSION_PRESETS = {
'boris-safe': {
name: "Boris's Safe Defaults",
desc: "Based on 19k tool uses - safe git, npm, file ops",
permissions: {
allow: [
"Bash(git remote:*)", "Bash(git status:*)", "Bash(git diff:*)", "Bash(git log:*)",
"Bash(git show:*)", "Bash(git rev-parse:*)", "Bash(git describe:*)", "Bash(git branch:*)",
"Bash(git tag:*)", "Bash(git add:*)", "Bash(git commit:*)", "Bash(git push:*)",
"Bash(npm run build:*)", "Bash(npm test:*)", "Bash(npm run test:*)", "Bash(npm run dev:*)",
"Bash(npm ci:*)", "Bash(npm install:*)",
"Bash(curl -s http://localhost:*)", "Bash(curl http://localhost:*)",
"Bash(cd:*)", "Bash(ls:*)", "Bash(pwd:*)", "Bash(cat:*)", "Bash(grep:*)", "Bash(find:*)",
"Bash(echo:*)", "Bash(date:*)", "Bash(wc:*)", "Bash(head:*)", "Bash(tail:*)", "Bash(jq:*)",
"Bash(sort:*)", "Bash(uniq:*)", "Bash(cut:*)", "Bash(tr:*)", "Bash(ps:*)", "Bash(lsof:*)",
"Bash(mkdir:*)", "Bash(chmod +x:*)", "Bash(cp:*)", "Bash(mv:*)", "Bash(touch:*)"
],
ask: [
"Bash(rm -rf:*)", "Bash(bash -c:*)", "Bash(ssh:*)"
],
deny: [
"Bash(rm -rf /:*)", "Bash(rm -rf ~:*)", "Bash(sudo rm:*)",
"Bash(sudo shutdown:*)", "Bash(sudo reboot:*)",
"Bash(chmod 777:*)", "Bash(chmod -R 777:*)"
]
}
},
'minimal': {
name: "Minimal Safe",
desc: "Read-only commands only",
permissions: {
allow: [
"Bash(git status:*)", "Bash(git diff:*)", "Bash(git log:*)",
"Bash(ls:*)", "Bash(pwd:*)", "Bash(cat:*)", "Bash(head:*)", "Bash(tail:*)"
],
ask: [
"Bash(git add:*)", "Bash(git commit:*)", "Bash(git push:*)",
"Bash(npm install:*)", "Bash(mkdir:*)", "Bash(cp:*)", "Bash(mv:*)"
],
deny: [
"Bash(rm -rf:*)", "Bash(sudo:*)", "Bash(chmod 777:*)"
]
}
},
'yolo': {
name: "YOLO Mode",
desc: "Allow almost everything (dangerous!)",
permissions: {
allow: [
"Bash(git:*)", "Bash(npm:*)", "Bash(node:*)", "Bash(bun:*)",
"Bash(ls:*)", "Bash(cd:*)", "Bash(cat:*)", "Bash(grep:*)", "Bash(find:*)",
"Bash(mkdir:*)", "Bash(cp:*)", "Bash(mv:*)", "Bash(rm:*)", "Bash(touch:*)",
"Bash(curl:*)", "Bash(wget:*)", "Bash(chmod:*)"
],
ask: ["Bash(sudo:*)"],
deny: ["Bash(rm -rf /:*)", "Bash(rm -rf ~:*)"]
}
}
};
function renderPermissions() {
const container = document.getElementById('permissions-container');
const perms = currentSettings.permissions || { allow: [], ask: [], deny: [] };
const sections = [
{ key: 'allow', label: 'Allow', desc: 'Auto-approved commands' },
{ key: 'ask', label: 'Ask', desc: 'Requires confirmation' },
{ key: 'deny', label: 'Deny', desc: 'Always blocked' }
];
// Preset selector
const presetHtml = `
<div style="margin-bottom: 16px; padding: 12px; background: var(--bg-tertiary); border-radius: 4px;">
<div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap;">
<span style="font-size: 12px; color: var(--text-secondary);">Load preset:</span>
${Object.entries(PERMISSION_PRESETS).map(([key, preset]) => `
<button class="btn-small" onclick="loadPreset('${key}')" title="${preset.desc}">
${preset.name}
</button>
`).join('')}
<button class="btn-small danger" onclick="clearPermissions()" title="Remove all rules">
Clear All
</button>
</div>
</div>
`;
container.innerHTML = presetHtml + sections.map(({ key, label, desc }) => {
const items = perms[key] || [];
return `
<div class="perm-section">
<div class="perm-header">
<span>
<span class="perm-badge ${key}">${label}</span>
<span style="color: var(--text-secondary); font-size: 11px; margin-left: 8px;">${desc}</span>
</span>
<span style="color: var(--text-secondary); font-size: 11px;">${items.length} rules</span>
</div>
<div class="perm-list">
${items.length === 0 ? '<div class="perm-item" style="color: var(--text-secondary);">No rules</div>' : ''}
${items.map((item, i) => `
<div class="perm-item">
<span>${item}</span>
<button class="btn-small danger" onclick="removePerm('${key}', ${i})">×</button>
</div>
`).join('')}
</div>
<div class="perm-input">
<input type="text" id="perm-input-${key}" placeholder="Bash(command:*)">
<button class="btn-small" onclick="addPerm('${key}')">+ Add</button>
</div>
</div>
`;
}).join('');
}
function addPerm(type) {
const input = document.getElementById(`perm-input-${type}`);
const value = input.value.trim();
if (!value) return;
if (!currentSettings.permissions) currentSettings.permissions = { allow: [], ask: [], deny: [] };
if (!currentSettings.permissions[type]) currentSettings.permissions[type] = [];
currentSettings.permissions[type].push(value);
input.value = '';
renderPermissions();
syncFormToJson();
}
function removePerm(type, index) {
currentSettings.permissions[type].splice(index, 1);
renderPermissions();
syncFormToJson();
}
function loadPreset(presetKey) {
const preset = PERMISSION_PRESETS[presetKey];
if (!preset) return;
if (!confirm(`Load "${preset.name}"?\n\n${preset.desc}\n\nThis will replace your current permissions.`)) {
return;
}
currentSettings.permissions = JSON.parse(JSON.stringify(preset.permissions));
renderPermissions();
syncFormToJson();
setStatus(`Loaded preset: ${preset.name}`, 'success');
}
function clearPermissions() {
if (!confirm('Clear all permission rules?')) return;
currentSettings.permissions = { allow: [], ask: [], deny: [] };
renderPermissions();
syncFormToJson();
setStatus('Cleared all permissions', 'success');
}
function renderHooks() {
const container = document.getElementById('hooks-container');
const hookTypes = ['SessionStart', 'SessionEnd', 'PreToolUse', 'PostToolUse'];
container.innerHTML = hookTypes.map(type => {
const hooks = currentSettings.hooks?.[type]?.[0]?.hooks || [];
return `
<div class="hooks-editor" style="margin-bottom: 12px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<strong style="font-size: 12px;">${type}</strong>
<button class="btn-small" onclick="addHook('${type}')">+ Add</button>
</div>
${hooks.map((hook, i) => `
<div class="hook-item">
<span class="hook-type">${hook.type}</span>
<input type="text" value="${hook.command || ''}"
onchange="updateHook('${type}', ${i}, this.value)">
<button class="btn-small danger" onclick="removeHook('${type}', ${i})">×</button>
</div>
`).join('')}
${hooks.length === 0 ? '<div style="color: var(--text-secondary); font-size: 12px;">No hooks configured</div>' : ''}
</div>
`;
}).join('');
}
function updateHook(type, index, value) {
if (!currentSettings.hooks) currentSettings.hooks = {};
if (!currentSettings.hooks[type]) currentSettings.hooks[type] = [{ hooks: [] }];
currentSettings.hooks[type][0].hooks[index].command = value;
syncFormToJson();
}
function addHook(type) {
if (!currentSettings.hooks) currentSettings.hooks = {};
if (!currentSettings.hooks[type]) currentSettings.hooks[type] = [{ hooks: [] }];
currentSettings.hooks[type][0].hooks.push({ type: 'command', command: '' });
renderHooks();
syncFormToJson();
}
function removeHook(type, index) {
currentSettings.hooks[type][0].hooks.splice(index, 1);
renderHooks();
syncFormToJson();
}
// Form change handlers
document.getElementById('setting-model').addEventListener('change', (e) => {
if (e.target.value) currentSettings.model = e.target.value;
else delete currentSettings.model;
syncFormToJson();
});
document.getElementById('setting-thinking').addEventListener('change', (e) => {
currentSettings.alwaysThinkingEnabled = e.target.checked;
syncFormToJson();
});
document.getElementById('setting-statusline-cmd').addEventListener('change', (e) => {
if (e.target.value) {
currentSettings.statusLine = { type: 'command', command: e.target.value, padding: 0 };
} else {
delete currentSettings.statusLine;
}
syncFormToJson();
});
function syncFormToJson() {
settingsEditor.setValue(JSON.stringify(currentSettings, null, 2));
}
function updatePreview() {
const md = claudeMdEditor.getValue();
document.getElementById('md-preview').innerHTML = marked.parse(md);
}
function setStatus(message, type = '') {
const bar = document.getElementById('status-bar');
const msg = document.getElementById('status-message');
bar.className = 'status-bar ' + type;
msg.textContent = message;
if (type) setTimeout(() => { bar.className = 'status-bar'; msg.textContent = 'Ready'; }, 3000);
}
function debounce(fn, ms) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), ms);
};
}
</script>
</body>
</html>
{
"name": "settings-ui",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"dependencies": {
"@hono/node-server": "^1.19.7",
"hono": "^4.11.3"
}
}
import { Hono } from 'hono'
import { serve } from '@hono/node-server'
import { serveStatic } from '@hono/node-server/serve-static'
import { readFile, writeFile, watch } from 'fs/promises'
import { createReadStream } from 'fs'
import { homedir } from 'os'
import { join } from 'path'
const app = new Hono()
const CLAUDE_DIR = join(homedir(), '.claude')
const SETTINGS_PATH = join(CLAUDE_DIR, 'settings.json')
const CLAUDE_MD_PATH = join(CLAUDE_DIR, 'CLAUDE.md')
// Serve static files
app.use('/static/*', serveStatic({ root: './' }))
// API: Get settings.json
app.get('/api/settings', async (c) => {
try {
const content = await readFile(SETTINGS_PATH, 'utf-8')
return c.json({ content, parsed: JSON.parse(content) })
} catch (e) {
return c.json({ error: e.message }, 500)
}
})
// API: Save settings.json
app.post('/api/settings', async (c) => {
try {
const { content } = await c.req.json()
// Validate JSON
JSON.parse(content)
await writeFile(SETTINGS_PATH, content, 'utf-8')
return c.json({ success: true })
} catch (e) {
return c.json({ error: e.message }, 400)
}
})
// API: Get CLAUDE.md
app.get('/api/claude-md', async (c) => {
try {
const content = await readFile(CLAUDE_MD_PATH, 'utf-8')
return c.json({ content })
} catch (e) {
return c.json({ error: e.message }, 500)
}
})
// API: Save CLAUDE.md
app.post('/api/claude-md', async (c) => {
try {
const { content } = await c.req.json()
await writeFile(CLAUDE_MD_PATH, content, 'utf-8')
return c.json({ success: true })
} catch (e) {
return c.json({ error: e.message }, 400)
}
})
// API: Get settings schema (for form generation)
app.get('/api/schema', (c) => {
return c.json({
properties: {
model: {
type: 'string',
enum: ['claude-sonnet-4-20250514', 'claude-opus-4-20250115', 'claude-opus-4-5-20251101'],
description: 'Default model to use'
},
alwaysThinkingEnabled: {
type: 'boolean',
description: 'Enable extended thinking by default'
},
statusLine: {
type: 'object',
description: 'Status line configuration',
properties: {
type: { type: 'string', enum: ['command'] },
command: { type: 'string' },
padding: { type: 'number' }
}
},
hooks: {
type: 'object',
description: 'Lifecycle hooks',
properties: {
SessionStart: { type: 'array' },
SessionEnd: { type: 'array' },
PreToolUse: { type: 'array' },
PostToolUse: { type: 'array' }
}
}
}
})
})
// Serve main HTML
app.get('/', async (c) => {
const html = await readFile(join(import.meta.dirname, 'static', 'index.html'), 'utf-8')
return c.html(html)
})
const port = 3456
console.log(`🔧 Claude Settings UI running at http://localhost:${port}`)
serve({ fetch: app.fetch, port })
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment