From 8cfa1b0d6afe8151291c589313ed145057622e01 Mon Sep 17 00:00:00 2001
From: r-zakarya <82443831+r-zakarya@users.noreply.github.com>
Date: Sun, 12 Jul 2026 11:48:08 +0100
Subject: [PATCH] feat(clipboard): user-configurable auto-clear delay
(Never/15/30/60/120s)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Single choke point: copySecure overrides any positive clearAfterMs with the
clipboardClearSeconds setting (0 = user disabled). The 15 call sites keep
passing 30000 unchanged — positive just means "auto-clear this secret";
explicit 0 (username copies) still never clears. Synced setting + a select in
Settings > Security (Clipboard privacy). Win+V history exclusion is
deliberately NOT exposed — a password manager must not offer to leak into
history / cloud clipboard.
Co-Authored-By: Claude Opus 4.8
---
index.html | 13 +++++++++++++
js/app.js | 21 +++++++++++++++++++--
2 files changed, 32 insertions(+), 2 deletions(-)
diff --git a/index.html b/index.html
index b949def..3cf5573 100644
--- a/index.html
+++ b/index.html
@@ -691,6 +691,19 @@
automatically when the app is minimised — no manual setup needed.
+
+
+ Auto-clear copied secrets after
+ Only clears if you haven't copied something else since.
+
+
+
diff --git a/js/app.js b/js/app.js
index 2da266d..ff01c02 100644
--- a/js/app.js
+++ b/js/app.js
@@ -68,12 +68,15 @@ const Bridge = (() => {
active,
// Copy text to clipboard, excluded from Win+V history.
- // clearAfterMs: Delphi auto-clears after this many ms (0 = never).
+ // clearAfterMs: 0 = never auto-clear (username copies). Any positive
+ // value means "auto-clear this secret" and is overridden by the user's
+ // clipboardClearSeconds setting (0 there = user disabled auto-clear).
// Returns true when the bridge handled the copy, false as fallback signal.
copySecure(text, clearAfterMs = 30000) {
if (!active) return false;
+ const ms = clearAfterMs > 0 ? (state.clipboardClearSeconds * 1000) : 0;
cmd('cmd://clipboard/copy?text=' + encodeURIComponent(text) +
- '&clear=' + clearAfterMs);
+ '&clear=' + ms);
return true;
},
@@ -646,6 +649,7 @@ const state = {
recoveryConfigured: false, // refreshed by refreshRecoveryStatus on Settings open
autofillEnabled: localStorage.getItem('autofillEnabled') !== '0', // default ON
autofillClearField: localStorage.getItem('autofillClearField') !== '0', // default ON
+ clipboardClearSeconds: parseInt(localStorage.getItem('clipboardClearSeconds') ?? '30', 10), // 0 = never
autofillFailBalloon: localStorage.getItem('autofillFailBalloon') !== '0', // default ON
// Hotkey combos. Each combo = { ctrl, shift, alt, win, key }.
// key is the uppercase character or VK label ('A'..'Z', '0'..'9',
@@ -6365,6 +6369,7 @@ function openSettings() {
$('#settingTrayNotifRow').style.display = Bridge.active ? '' : 'none';
$('#settingTrashPurge').value = String(state.trashAutoPurgeDays || 0);
$('#settingPasswordExpiry').value = String(state.passwordExpiryDays || 0);
+ $('#settingClipboardClear').value = String(state.clipboardClearSeconds ?? 30);
$('#settingEditorPosition').value = state.editorPosition || 'right';
$('#settingConfirmUnsaved').checked = state.confirmOnUnsaved !== false;
// PIN unlock — only meaningful when DPAPI is available.
@@ -6761,6 +6766,8 @@ const SYNCED_SETTING_KEYS = [
'autofillClearField',
// Tray balloon when a fill fails while the window is hidden (default ON).
'autofillFailBalloon',
+ // Seconds before a copied secret is auto-cleared (0 = never). Default 30.
+ 'clipboardClearSeconds',
];
// Sets `data-editor-position` on so CSS can swap the slideover
@@ -6847,6 +6854,9 @@ async function loadServerSettings() {
case 'autofillFailBalloon':
localStorage.setItem('autofillFailBalloon', v ? '1' : '0');
break;
+ case 'clipboardClearSeconds':
+ localStorage.setItem('clipboardClearSeconds', String(v));
+ break;
}
});
// Apply visual settings immediately.
@@ -7531,6 +7541,13 @@ async function init() {
if (n === 0) toast('Trash auto-purge disabled');
else toast('Trash will auto-purge after ' + n + ' days (next unlock)');
});
+ $('#settingClipboardClear').addEventListener('change', e => {
+ const n = parseInt(e.target.value, 10) || 0;
+ state.clipboardClearSeconds = n;
+ localStorage.setItem('clipboardClearSeconds', String(n));
+ saveServerSettings();
+ toast(n === 0 ? 'Clipboard auto-clear disabled' : 'Copied secrets clear after ' + n + 's');
+ });
$('#settingPasswordExpiry').addEventListener('change', e => {
const n = parseInt(e.target.value, 10) || 0;
state.passwordExpiryDays = n;