feat(diagnostic): Datenschutz-Schalter von Claude Code pruefen und setzen

Drei Kanaele, ueber die Claude Code zusaetzlich zur eigentlichen Anfrage Daten
verschickt — bisher nur per Hand in der docker-compose.yml abschaltbar, und
niemand konnte sehen, ob sie an sind:

  DISABLE_ERROR_REPORTING   Stacktraces an einen Fehler-Dienst. Bei Pro/Max ab
                            CLI 2.1.198 standardmaessig AN.
  DISABLE_TELEMETRY         Nutzungs-Metriken.
  DISABLE_FEEDBACK_COMMAND  /feedback sendet sonst die ganze Unterhaltung
                            inkl. Code — 5 Jahre Aufbewahrung.

Gesetzt wird in ~/.claude/settings.json unter "env". Die Datei ist in den
Proxy-Container gemountet, die Diagnostic erreicht sie ueber den vorhandenen
dockerExec — also kein Anfassen der docker-compose.yml und KEIN Neustart: die
CLI liest die Datei pro Aufruf.

Geprueft wird beides: was in settings.json steht UND was im Prozess wirklich
ankommt. Eine echte Container-Umgebungsvariable gewinnt gegen settings.json —
so ein Eintrag wird darum als "per docker-compose gesetzt" angezeigt und der
Haken gesperrt, statt eine Einstellung anzubieten, die nichts tut.

Geschrieben wird atomar (temp + mv) und nur der env-Block angefasst; bestehende
Einstellungen (tui, theme) bleiben. Sind alle drei abgewaehlt, verschwindet der
env-Block wieder ganz.

Der Hinweis im Panel sagt ausdruecklich, dass das NICHT die Trainings-
Einstellung ist — die steht im Anthropic-Konto und laesst sich hier nicht
umstellen. Link dorthin ist daneben.

12 Pruefungen gegen den echten Proxy-Container: lesen, setzen, Teilmengen,
JSON-Integritaet, Erhalt bestehender Werte, CLI laeuft damit, Abwaehlen raeumt
auf, Ausgangszustand wiederhergestellt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-03 11:21:47 +02:00
co-authored by Claude Opus 5
parent 2c60a19ed6
commit d5eea920a6
2 changed files with 191 additions and 0 deletions
+96
View File
@@ -654,6 +654,31 @@
</div>
</div>
<div class="settings-section">
<h2>Datenschutz — was Claude Code von sich aus sendet</h2>
<div class="card" style="max-width:620px;">
<div style="font-size:11px;color:#8888AA;margin-bottom:12px;line-height:1.55;">
Drei Kanäle, über die Claude Code <strong>zusätzlich</strong> zur eigentlichen
Anfrage Daten verschickt. Gesetzt wird in <code>~/.claude/settings.json</code> —
greift bei jeder neuen Anfrage, <strong>kein Neustart nötig</strong>.
<br><br>
Davon unabhängig ist, ob Anthropic mit deinen Unterhaltungen Modelle
trainiert — das steht in deinem Konto unter
<a href="https://claude.ai/settings/data-privacy-controls" target="_blank"
style="color:#0096FF;">Datenschutz → „Help improve our AI models"</a>
und lässt sich hier nicht umstellen.
</div>
<div id="privacy-flags-list" style="display:flex;flex-direction:column;gap:10px;">
<span style="color:#555570;font-size:11px;">(noch nicht geprüft)</span>
</div>
<div style="display:flex;gap:8px;margin-top:14px;flex-wrap:wrap;">
<button class="btn secondary" onclick="loadPrivacyFlags()" style="padding:5px 12px;font-size:11px;">🔍 Prüfen</button>
<button class="btn" onclick="savePrivacyFlags()" style="padding:5px 12px;font-size:11px;">Speichern</button>
</div>
<div id="privacy-flags-status" style="margin-top:10px;font-size:11px;color:#8888AA;white-space:pre-wrap;"></div>
</div>
</div>
<div class="settings-section">
<h2>Lokales LLM (schnelle Antworten) <button class="info-btn" onclick="showInfo('local-llm')" title="Wie funktioniert das?">ℹ</button></h2>
<div class="card" style="max-width:540px;">
@@ -2723,6 +2748,14 @@
return;
}
if (msg.type === 'privacy_flags') { renderPrivacyFlags(msg); return; }
if (msg.type === 'privacy_flags_saved') {
const st = document.getElementById('privacy-flags-status');
if (st && msg.error) { st.textContent = 'Speichern fehlgeschlagen: ' + msg.error; st.style.color = '#FF6B6B'; }
// Erfolg: der direkt folgende privacy_flags-Broadcast malt den Stand neu.
return;
}
if (msg.type === 'model_test_result') {
const box = document.getElementById('model-test-result');
const btn = document.getElementById('btn-test-model');
@@ -4720,6 +4753,7 @@
loadJellyfinConfig();
refreshVoiceIdStatus();
send({ action: 'get_attachment_limit' });
send({ action: 'get_privacy_flags' });
} else if (tab === 'brain') {
loadBrainStatus();
loadBrainMemoryList();
@@ -8019,6 +8053,68 @@
}
}
// ── Datenschutz-Schalter ──────────────────────────────────────────
function loadPrivacyFlags() {
const st = document.getElementById('privacy-flags-status');
if (st) { st.textContent = 'Lese den Proxy-Container …'; st.style.color = '#8888AA'; }
send({ action: 'get_privacy_flags' });
}
function savePrivacyFlags() {
const flags = {};
document.querySelectorAll('#privacy-flags-list input[type=checkbox][data-key]')
.forEach(cb => { flags[cb.dataset.key] = cb.checked; });
if (!Object.keys(flags).length) {
const st = document.getElementById('privacy-flags-status');
if (st) { st.textContent = 'Erst „Prüfen" drücken.'; st.style.color = '#FFD60A'; }
return;
}
send({ action: 'set_privacy_flags', flags });
}
function renderPrivacyFlags(msg) {
const box = document.getElementById('privacy-flags-list');
const st = document.getElementById('privacy-flags-status');
if (!box) return;
if (msg.error) {
box.innerHTML = '';
if (st) { st.textContent = 'Fehler: ' + msg.error; st.style.color = '#FF6B6B'; }
return;
}
box.innerHTML = (msg.flags || []).map(f => {
// "container" = per docker-compose gesetzt. Das gewinnt gegen
// settings.json, also waere ein Haken hier wirkungslos — darum sperren
// und ehrlich sagen warum.
const locked = f.source === 'container';
return '<label style="display:flex;gap:10px;align-items:flex-start;cursor:' + (locked ? 'default' : 'pointer') + ';">'
+ '<input type="checkbox" data-key="' + f.key + '"' + (f.disabled ? ' checked' : '')
+ (locked ? ' disabled' : '') + ' style="margin-top:3px;">'
+ '<span style="flex:1;">'
+ '<span style="color:' + (f.disabled ? '#3FFF9F' : '#FFD60A') + ';font-size:12px;">'
+ (f.disabled ? '✓ ' : '⚠ ') + escapeHtml(f.label) + '</span>'
+ '<span style="color:#555570;font-size:10px;"> — ' + escapeHtml(f.key) + '</span>'
+ '<br><span style="color:#8888AA;font-size:10px;">' + escapeHtml(f.info) + '</span>'
+ (locked ? '<br><span style="color:#FF9F0A;font-size:10px;">Per docker-compose gesetzt — '
+ 'das gewinnt gegen diese Einstellung und lässt sich hier nicht ändern.</span>' : '')
+ '</span></label>';
}).join('') || '<span style="color:#555570;font-size:11px;">(keine Angaben)</span>';
const offen = (msg.flags || []).filter(f => !f.disabled).length;
if (st) {
if (msg.bulkDisabled) {
st.textContent = 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC ist gesetzt — das schaltet ohnehin alles ab.';
st.style.color = '#3FFF9F';
} else if (offen === 0) {
st.textContent = '✅ Alle drei Kanäle sind zu.';
st.style.color = '#3FFF9F';
} else {
st.textContent = '⚠ ' + offen + ' von ' + (msg.flags || []).length + ' Kanälen senden noch. '
+ 'Haken setzen und speichern.';
st.style.color = '#FFD60A';
}
}
}
/**
* Testet ein Modell mit einer echten Mini-Anfrage.
* fromFreeText=true nimmt die handgetippte ID, sonst das Dropdown.
+95
View File
@@ -3217,6 +3217,10 @@ wss.on("connection", (ws) => {
handleSetModel(ws, msg.model);
} else if (msg.action === "test_model") {
handleTestModel(ws, msg.model);
} else if (msg.action === "get_privacy_flags") {
handleGetPrivacyFlags(ws);
} else if (msg.action === "set_privacy_flags") {
handleSetPrivacyFlags(ws, msg.flags);
} else if (msg.action === "get_attachment_limit") {
handleGetAttachmentLimit(ws);
} else if (msg.action === "set_attachment_limit") {
@@ -3643,6 +3647,97 @@ function handleSetModel(clientWs, model) {
return;
}
// ── Einstellungen: Datenschutz-Schalter von Claude Code ─────────────
//
// Drei Kanaele, ueber die Claude Code von sich aus Daten an Anthropic bzw. an
// einen Fehler-Dienst schickt. Sie liegen als Umgebungsvariablen vor und lassen
// sich in ~/.claude/settings.json unter "env" setzen — die Datei ist in den
// Proxy-Container gemountet, also koennen wir sie von hier aus pflegen, ohne
// die docker-compose.yml anzufassen.
//
// WICHTIG: gesetzt wird in settings.json. Steht dieselbe Variable ZUSAETZLICH
// als echte Container-Umgebungsvariable (docker-compose), gewinnt die — darum
// prueft "Pruefen" beides und sagt, was tatsaechlich im Prozess ankommt.
const PRIVACY_FLAGS = [
{ key: "DISABLE_ERROR_REPORTING",
label: "Fehlerberichte",
info: "Stacktraces an einen Fehler-Dienst. Bei Pro/Max ab CLI 2.1.198 standardmäßig AN." },
{ key: "DISABLE_TELEMETRY",
label: "Nutzungs-Metriken",
info: "Latenz/Zuverlässigkeit. Enthält laut Anthropic nie Code, Prompts oder Pfade." },
{ key: "DISABLE_FEEDBACK_COMMAND",
label: "/feedback-Versand",
info: "Sendet sonst auf Zuruf die ganze Unterhaltung inkl. Code — 5 Jahre Aufbewahrung." },
];
const CLAUDE_SETTINGS_PATH = "/root/.claude/settings.json";
function _shq(s) { return "'" + String(s).replace(/'/g, "'\\''") + "'"; }
async function handleGetPrivacyFlags(clientWs) {
try {
// 1) Was kommt im PROZESS wirklich an? Das ist die Wahrheit.
const envOut = await dockerExec("aria-proxy",
"env | grep -E '^(DISABLE_ERROR_REPORTING|DISABLE_TELEMETRY|DISABLE_FEEDBACK_COMMAND|CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC)=' || true");
const liveEnv = {};
for (const line of String(envOut || "").split("\n")) {
const i = line.indexOf("=");
if (i > 0) liveEnv[line.slice(0, i).trim()] = line.slice(i + 1).trim();
}
// 2) Was steht in settings.json?
const settingsRaw = await dockerExec("aria-proxy",
`cat ${CLAUDE_SETTINGS_PATH} 2>/dev/null || echo '{}'`);
let settingsEnv = {};
try { settingsEnv = (JSON.parse(settingsRaw || "{}").env) || {}; } catch (_) {}
const isOn = (v) => v === "1" || String(v).toLowerCase() === "true";
const flags = PRIVACY_FLAGS.map(f => {
const fromEnv = liveEnv[f.key];
const fromSettings = settingsEnv[f.key];
// Container-Env gewinnt gegen settings.json.
const effective = fromEnv !== undefined ? isOn(fromEnv) : isOn(fromSettings);
return {
key: f.key, label: f.label, info: f.info,
disabled: effective,
source: fromEnv !== undefined ? "container" : (fromSettings !== undefined ? "settings" : "none"),
};
});
const bulk = isOn(liveEnv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC);
clientWs.send(JSON.stringify({ type: "privacy_flags", flags, bulkDisabled: bulk }));
} catch (err) {
clientWs.send(JSON.stringify({ type: "privacy_flags", error: String((err && err.message) || err) }));
}
}
async function handleSetPrivacyFlags(clientWs, wanted) {
try {
const keys = PRIVACY_FLAGS.map(f => f.key);
const raw = await dockerExec("aria-proxy",
`cat ${CLAUDE_SETTINGS_PATH} 2>/dev/null || echo '{}'`);
let cfg = {};
try { cfg = JSON.parse(raw || "{}"); } catch (_) { cfg = {}; }
if (!cfg.env || typeof cfg.env !== "object") cfg.env = {};
for (const k of keys) {
if (wanted && wanted[k]) cfg.env[k] = "1";
else delete cfg.env[k];
}
if (Object.keys(cfg.env).length === 0) delete cfg.env;
// Atomar schreiben (temp + mv), damit eine laufende CLI nie eine halbe
// Datei liest.
const json = JSON.stringify(cfg, null, 2);
await dockerExec("aria-proxy",
`printf '%s' ${_shq(json)} > ${CLAUDE_SETTINGS_PATH}.tmp && mv ${CLAUDE_SETTINGS_PATH}.tmp ${CLAUDE_SETTINGS_PATH}`);
log("info", "server", `Datenschutz-Schalter gesetzt: ${JSON.stringify(wanted || {})}`);
clientWs.send(JSON.stringify({
type: "privacy_flags_saved",
info: "Gespeichert. Gilt für JEDE neue Anfrage — ein laufender Turn nutzt noch die alte Einstellung.",
}));
await handleGetPrivacyFlags(clientWs);
} catch (err) {
clientWs.send(JSON.stringify({ type: "privacy_flags_saved", error: String((err && err.message) || err) }));
}
}
// ── Einstellungen: Model-Test ───────────────────────────────────────
//
// Schickt eine winzige ECHTE Anfrage durch den kompletten Weg