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
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

All notable changes to the Official Dotenv VS Code extension will be documented in this file.

## [Unreleased](https://github.com/dotenvx/dotenv-vscode/compare/v1.5.7...master)
## [Unreleased](https://github.com/dotenvx/dotenv-vscode/compare/v1.5.8...master)

## [1.5.8](https://github.com/dotenvx/dotenv-vscode/compare/v1.5.7...v1.5.8) (2026-09-23)

### Changed

* Get settings right - turning off the feature not the cloaking. ([#143](https://github.com/dotenvx/dotenv-vscode/pull/143))

## [1.5.7](https://github.com/dotenvx/dotenv-vscode/compare/v1.5.6...v1.5.7) (2026-09-23)

Expand Down Expand Up @@ -183,6 +189,8 @@ All notable changes to the Official Dotenv VS Code extension will be documented

### Fixed

* Always cloak new secret peeks, independently of editor cloaking. Disable cloaking controls when the auto-cloaking feature is turned off.

* Disable dotenv hovers and expanded autocomplete value details when secret peeking is turned off, while keeping autocomplete suggestions available.

* Reverted code causing autocloaking to fail [#93](https://github.com/dotenvx/dotenv-vscode/pull/93)
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ from the Command Palette to reveal or hide them.

Switching tabs hides them again in the Dotenv Editor when auto-cloaking is enabled.
Set `dotenv.enableAutocloaking` to `false` to keep values visible, including after
reopening files or switching tabs. Changes to this setting apply to open editors immediately.
reopening files or switching tabs. This disables the cloaking feature and removes
its toggle controls. Changes apply to open editors immediately.

 

Expand Down Expand Up @@ -96,7 +97,9 @@ secret-peeking setting stays unchanged.

Hover over a reference such as `process.env.SECRET_KEY` or `ENV["SECRET_KEY"]`.

The popup shows the source file and lets you **Reveal value** or **Hide value**.
Every new hover or expanded autocomplete peek starts cloaked. Click **Reveal value**
to reveal that popup, or **Hide value** to cloak it again. Editor cloaking toggles
never reveal peek values.
Uncheck **Dotenv: Enable Secretpeeking** to disable dotenv hovers and in-code
Reveal actions, including the expanded value details in autocomplete. Completion
suggestions remain available with masked values.
Expand Down
9 changes: 8 additions & 1 deletion lib/autocloaking.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ function decorateMasking (context) {

const toggleLink = {
provideCodeLenses: function (document, token) {
if (!settings.autocloakingFeatureEnabled(document.uri)) return []
if (yaml.supported(document) && !yaml.ranges(document).length) return []
const range = new vscode.Range(0, 0, 0, 0) // place at top of file
const lens = new vscode.CodeLens(range, {
Expand All @@ -71,7 +72,12 @@ const toggleLink = {
}

function buildToggle (context) {
const codeLens = vscode.languages.registerCodeLensProvider([{ language: 'dotenv' }, { language: 'yaml' }], toggleLink)
const codeLens = vscode.languages.registerCodeLensProvider([{ language: 'dotenv' }, { language: 'yaml' }], {
...toggleLink,
onDidChangeCodeLenses: listener => vscode.workspace.onDidChangeConfiguration(event => {
if (event.affectsConfiguration('dotenv.enableAutocloaking')) listener()
})
})

context.subscriptions.push(codeLens)

Expand All @@ -80,6 +86,7 @@ function buildToggle (context) {

async function dotenvToggleAutocloaking () {
const uri = vscode.window.activeTextEditor?.document.uri
if (!settings.autocloakingFeatureEnabled(uri)) return false
if (settings.autocloakingEnabled(uri)) {
await settings.autocloakingOff(uri)
} else {
Expand Down
2 changes: 1 addition & 1 deletion lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function valueHover (key, document, range) {
if (!settings.secretpeekingEnabled(document.uri)) return undefined
const values = module.exports.envValues(document).get(key)
if (!values) return new vscode.Hover(settings.missingText(), range)
const revealed = hoverReveal.begin(document, key, range, settings.secretpeekingEnabled(document.uri))
const revealed = hoverReveal.begin(document, key, range)
const contents = [valueDocumentation(values, undefined, undefined, revealed, document.uri)]
const control = values.some(entry => entry.value) && hoverReveal.control(document, key, range, revealed)
if (control) contents.push(control)
Expand Down
4 changes: 2 additions & 2 deletions lib/hover-reveal.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ function matches (request, document, key, range) {
request.version === document.version && request.key === key && range && request.range.isEqual(range)
}

function begin (document, key, range, defaultRevealed) {
function begin (document, key, range) {
const request = pending
if (!matches(request, document, key, range)) return defaultRevealed
if (!matches(request, document, key, range)) return false
pending = undefined
return request.revealed
}
Expand Down
4 changes: 2 additions & 2 deletions lib/secure-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function html (webview, extensionUri) {
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${webview.cspSource} data:; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}' ${webview.cspSource}; worker-src blob:; connect-src ${webview.cspSource};">
<link rel="stylesheet" href="${uri('media/editor/dist/main.css')}">
<link rel="stylesheet" href="${uri('media/editor/style.css')}"><title>Dotenv</title></head><body>
<div class="lens"><button id="toggle" disabled>Toggle auto-cloaking</button><span id="status" role="status">Loading…</span></div>
<div class="lens"><button id="toggle" hidden disabled>Toggle auto-cloaking</button><span id="status" role="status">Loading…</span></div>
<div id="editor" class="preparing" data-worker="${uri('media/editor/dist/worker.js')}"></div>
<script nonce="${nonce}" src="${uri('media/editor/dist/main.js')}"></script></body></html>`
}
Expand All @@ -38,7 +38,7 @@ function resolveCustomTextEditor (document, panel, context) {
const send = (extra = {}) => {
if (client && !disposed) webview.postMessage({ type: 'document', client, text: text(), version: document.version, filename: path.basename(document.uri.fsPath || ''), options: options(), ...configuration(), ...extra })
}
const view = { panel, toggle: () => webview.postMessage({ type: 'toggle' }) }
const view = { panel, toggle: () => { if (configuration().autocloaking) webview.postMessage({ type: 'toggle' }) } }
views.add(view)
const subscriptions = [
vscode.workspace.onDidChangeTextDocument(event => {
Expand Down
9 changes: 8 additions & 1 deletion lib/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ function initialize (context) {
}

async function setAutocloaking (enabled, uri) {
if (!autocloakingFeatureEnabled(uri)) return false
const override = { enabled, configured: !!userConfig(uri).get(enableAutocloakingKey), scope: scope(uri) }
overrides = overrides.filter(item => item.scope !== override.scope).concat(override)
await extensionState.update(toggleStateKey, overrides)
Expand Down Expand Up @@ -80,8 +81,13 @@ function userConfig (uri) {
}

// settings
function autocloakingFeatureEnabled (uri) {
return !!userConfig(uri).get(enableAutocloakingKey)
}

function autocloakingEnabled (uri) {
const configured = !!userConfig(uri).get(enableAutocloakingKey)
const configured = autocloakingFeatureEnabled(uri)
if (!configured) return false
const override = overrides.find(item => item.scope === scope(uri))
return override && override.configured === configured ? override.enabled : configured
}
Expand Down Expand Up @@ -112,6 +118,7 @@ module.exports.resetAutocloaking = resetAutocloaking
module.exports.removeLegacyMask = removeLegacyMask

// settings
module.exports.autocloakingFeatureEnabled = autocloakingFeatureEnabled
module.exports.autocloakingEnabled = autocloakingEnabled
module.exports.secretpeekingEnabled = secretpeekingEnabled
module.exports.cloakColor = cloakColor
Expand Down
6 changes: 4 additions & 2 deletions media/editor/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ function applyMask () {
})
: [])
status.textContent = conflict ? 'File changed elsewhere. Copy your edits before reopening.' : ''
toggle.hidden = !autocloaking
toggle.disabled = !autocloaking
toggle.setAttribute('aria-label', masked ? 'Reveal dotenv values' : 'Hide dotenv values')
}
function conceal () {
Expand All @@ -128,7 +130,7 @@ function conceal () {
}
function toggleMask () {
encryptedHover?.hide()
if (!editor) return
if (!editor || !autocloaking) return
container.classList.add('preparing')
masked = !masked
applyMask()
Expand Down Expand Up @@ -210,7 +212,7 @@ function updateDocument (message) {
previous = message.text
applyMask()
editor.render(true)
toggle.disabled = false
toggle.disabled = !autocloaking
if (!document.hidden) container.classList.remove('preparing')
}
window.addEventListener('message', event => {
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@
"scope": "resource",
"type": "boolean",
"default": true,
"description": "Enable auto-cloaking for .env files and environment values in YAML"
"description": "Enable cloaking and its toggle controls for .env files and environment values in YAML"
},
"dotenv.enableSecretpeeking": {
"scope": "resource",
"type": "boolean",
"default": true,
"description": "Enable in-code secret peeking for your environment variables"
"description": "Enable in-code secret peeking. Values start cloaked until you click Reveal value."
},
"dotenv.cloakColor": {
"scope": "resource",
Expand Down
5 changes: 4 additions & 1 deletion test/renderer/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import { sourceEditor } from '../../media/editor/main.js'

window.addEventListener('message', async event => {
if (event.data?.type !== 'checkCloaking') return
const { masked, id } = event.data
const { masked, id, featureEnabled = masked, clickToggle = false } = event.data
try {
if (clickToggle) document.getElementById('toggle').click()
for (let i = 0; i < 100; i++) {
if (sourceEditor && document.getElementById('toggle').getAttribute('aria-label') === (masked ? 'Reveal dotenv values' : 'Hide dotenv values')) break
await new Promise(resolve => setTimeout(resolve, 25))
}
for (let i = 0; i < 3; i++) await new Promise(resolve => requestAnimationFrame(resolve))
const toggle = document.getElementById('toggle')
if (toggle.hidden === featureEnabled || toggle.disabled === featureEnabled) throw new Error('Toggle availability must follow feature enablement')
const spans = [...document.querySelectorAll('.view-line span')].filter(span => !span.children.length && span.textContent.includes('SECRET_CONFIGURATION'))
if (!spans.length) throw new Error('Secret text was not rendered')
for (const span of spans) {
Expand Down
28 changes: 28 additions & 0 deletions test/suite/extension.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,34 @@ SINGLE='secret'
}
})

it('removes native cloaking controls and ignores toggles while the feature is disabled', async function () {
this.timeout(15000)
const uri = vscode.Uri.joinPath(vscode.workspace.workspaceFolders[0].uri, '.dev.vars')
const document = await vscode.workspace.openTextDocument(uri)
await vscode.window.showTextDocument(document)
const config = vscode.workspace.getConfiguration('dotenv', uri)
const original = config.inspect('enableAutocloaking').workspaceValue
const settings = require('../../lib/settings')
let applied
const editor = { document, setDecorations: (_, ranges) => { applied = ranges } }
try {
for (const enabled of [true, false, true]) {
await config.update('enableAutocloaking', enabled, vscode.ConfigurationTarget.Workspace)
const lenses = await vscode.commands.executeCommand('vscode.executeCodeLensProvider', uri)
assert.strictEqual(lenses.some(lens => lens.command.command === 'dotenv.toggleAutocloaking'), enabled)
if (!enabled) {
await vscode.commands.executeCommand('dotenv.toggleAutocloaking')
assert.strictEqual(settings.autocloakingEnabled(uri), false)
}
decorations.decorate({}, editor)
assert.strictEqual(applied.length, enabled ? 1 : 0)
}
} finally {
await settings.resetAutocloaking()
await config.update('enableAutocloaking', original, vscode.ConfigurationTarget.Workspace)
}
})

it('clears the cloak and removes the toggle when switching language modes', async function () {
const uri = vscode.Uri.joinPath(vscode.workspace.workspaceFolders[0].uri, '.dev.vars')
let document = await vscode.workspace.openTextDocument(uri)
Expand Down
3 changes: 2 additions & 1 deletion test/suite/lib/autocloaking.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ function setup () {
const editor = uri => ({ document: { uri: { toString: () => uri } } })
const editors = [editor('file:///project/.env'), editor('file:///project/.env')]
const settings = {
autocloakingFeatureEnabled: () => true,
autocloakingEnabled: () => enabled,
initialize: () => {},
removeLegacyMask: async () => {},
Expand Down Expand Up @@ -219,7 +220,7 @@ describe('cloaking settings isolation', () => {
configure(false)
assert.strictEqual(settings.autocloakingEnabled(), false)
await settings.autocloakingOn()
assert.strictEqual(settings.autocloakingEnabled(), true)
assert.strictEqual(settings.autocloakingEnabled(), false)
await settings.resetAutocloaking()
assert.strictEqual(settings.autocloakingEnabled(), false)
})
Expand Down
12 changes: 7 additions & 5 deletions test/suite/lib/dotnet.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,17 @@ describe('.NET hover', () => {
]) {
it(`shows the .env value: ${text}`, () => {
const result = dotnet.hover.provideHover(document(text), new vscode.Position(0, text.indexOf('HELLO') + 1))
assert(result.contents[0].value.includes('World'))
assert(result.contents[0].value.includes('█████'))
assert(!result.contents[0].value.includes('World'))
assert.strictEqual(result.range.start.character, text.indexOf('HELLO'))
})
}

it('selects the hovered call when several calls or repeated names share a line', () => {
const text = 'var HELLO = Environment.GetEnvironmentVariable("UNKNOWN") + Environment.GetEnvironmentVariable("HELLO");'
const result = dotnet.hover.provideHover(document(text), new vscode.Position(0, text.lastIndexOf('HELLO') + 1))
assert(result.contents[0].value.includes('World'))
assert(result.contents[0].value.includes('█████'))
assert(!result.contents[0].value.includes('World'))
assert.strictEqual(dotnet.hover.provideHover(document(text), new vscode.Position(0, 5)), undefined)
})

Expand All @@ -74,7 +76,7 @@ describe('.NET hover', () => {
try {
helpers.envValues = () => new Map(Object.entries({ lower_key: 'x', EMPTY: '' }).map(([key, value]) => [key, [{ value, source: '.env' }]]))
settings.secretpeekingEnabled = () => false
for (const [key, expected] of [['lower_key', 'x'], ['EMPTY', '(empty)']]) {
for (const [key, expected] of [['lower_key', '█'], ['EMPTY', '(empty)']]) {
const text = `Environment.GetEnvironmentVariable("${key}")`
settings.secretpeekingEnabled = () => false
assert.strictEqual(dotnet.hover.provideHover(document(text), new vscode.Position(0, text.indexOf(key))), undefined)
Expand All @@ -83,7 +85,7 @@ describe('.NET hover', () => {
}
const text = 'Environment.GetEnvironmentVariable("HELLO")'
helpers.envValues = () => new Map(Object.entries({ HELLO: 'World' }).map(([key, value]) => [key, [{ value, source: '.env' }]]))
assert(dotnet.hover.provideHover(document(text), new vscode.Position(0, text.indexOf('HELLO'))).contents[0].value.includes('World'))
assert(dotnet.hover.provideHover(document(text), new vscode.Position(0, text.indexOf('HELLO'))).contents[0].value.includes('█████'))
helpers.envValues = () => new Map()
assert.strictEqual(dotnet.hover.provideHover(document(text), new vscode.Position(0, text.indexOf('HELLO'))).contents[0], settings.missingText())
} finally {
Expand All @@ -104,7 +106,7 @@ describe('.NET registered providers', () => {
const doc = await vscode.workspace.openTextDocument(uri)
await vscode.languages.setTextDocumentLanguage(doc, language)
const hover = await vscode.commands.executeCommand('vscode.executeHoverProvider', uri, new vscode.Position(0, text.indexOf('HELLO') + 1))
assert(hover.some(item => item.contents.some(content => (content.value || content).includes('World'))))
assert(hover.some(item => item.contents.some(content => (content.value || content).includes('█████'))))
const offset = text.split('\n')[1].indexOf('HE') + 2
const completions = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', uri, new vscode.Position(1, offset))
assert(completions.items.some(item => (item.label.label || item.label) === 'HELLO'))
Expand Down
12 changes: 7 additions & 5 deletions test/suite/lib/env-discovery-providers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ describe('dotenv discovery through language providers', () => {
assert(!item.documentation.value.includes('productionvalue'))
const hovers = await vscode.commands.executeCommand('vscode.executeHoverProvider', uri, new vscode.Position(1, reference.indexOf('DISCOVERY_KEY') + 2))
const content = hovers.flatMap(hover => hover.contents).map(value => value.value || value).join('\n')
assert(content.includes('localvalue'))
assert(content.includes('productionvalue'))
assert(content.includes('█'.repeat('localvalue'.length)))
assert(!content.includes('localvalue'))
assert(!content.includes('productionvalue'))
assert(content.includes('.env.local'))
assert(content.includes('.env.production'))
assert.strictEqual(document.getText(), `${complete}\n${reference}`)
Expand All @@ -64,8 +65,9 @@ describe('dotenv discovery through language providers', () => {
assert(!item.documentation.value.includes('localvalue'))
const hovers = await vscode.commands.executeCommand('vscode.executeHoverProvider', uri, new vscode.Position(2, 8))
const content = hovers.flatMap(hover => hover.contents).map(value => value.value || value).join('\n')
assert(content.includes('localvalue'))
assert(content.includes('productionvalue'))
assert(content.includes('█'.repeat('localvalue'.length)))
assert(!content.includes('localvalue'))
assert(!content.includes('productionvalue'))
})
}

Expand Down Expand Up @@ -102,7 +104,7 @@ describe('dotenv discovery through language providers', () => {
assert.strictEqual(helpers.valueHover('MASK_TEST', document), undefined)
}
settings.secretpeekingEnabled = () => true
assert(helpers.valueHover('MASK_TEST', document).contents[0].value.includes('🌴secret'))
assert(helpers.valueHover('MASK_TEST', document).contents[0].value.includes('█'.repeat('🌴secret'.length)))
} finally {
settings.secretpeekingEnabled = originalPeeking
settings.cloakIcon = originalIcon
Expand Down
Loading
Loading