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

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.6...master)
## [Unreleased](https://github.com/dotenvx/dotenv-vscode/compare/v1.5.7...master)

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

### Changed

* Respect secret-peeking global setting when off.

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

### Changed

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

### Fixed

* 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)

## 0.24.0
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,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**.
New hovers follow your `dotenv.enableSecretpeeking` setting.
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
3 changes: 3 additions & 0 deletions lib/completion-reveal.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const vscode = require('vscode')
const crypto = require('crypto')
const settings = require('./settings')
const command = 'dotenv.toggleCompletionValue'
const links = new Map()
const batches = new WeakMap()
Expand Down Expand Up @@ -69,11 +70,13 @@ function run (context) {
const editor = vscode.window.activeTextEditor
if (running || !request || request.expires < Date.now() || !editor || !matches(request.batch, editor.document, editor.selection.active)) return
links.delete(id)
if (!settings.secretpeekingEnabled(editor.document.uri)) return
running = true
try {
await vscode.commands.executeCommand('hideSuggestWidget')
const active = vscode.window.activeTextEditor
if (!active || !matches(request.batch, active.document, active.selection.active)) return
if (!settings.secretpeekingEnabled(active.document.uri)) return
pending = request
await vscode.commands.executeCommand('editor.action.triggerSuggest')
} finally {
Expand Down
8 changes: 6 additions & 2 deletions lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,22 @@ function displayValue (value, revealed, uri) {
}

function valueDocumentation (values, batch, key, hoverRevealed, uri) {
const peeking = settings.secretpeekingEnabled(uri)
if (!peeking) return undefined
const doc = new vscode.MarkdownString()
for (const { source, value } of values) {
doc.appendText(source)
doc.appendMarkdown('\n\n')
const revealed = batch?.request && batch.request.key === key && batch.request.source === source && batch.request.revealed
const revealed = peeking && batch?.request && batch.request.key === key && batch.request.source === source && batch.request.revealed
doc.appendText(batch ? ((revealed ? value : _mask(value, uri)) || '(empty)') : displayValue(value, hoverRevealed, uri))
if (batch && value) completionReveal.append(doc, batch, key, source, revealed)
if (peeking && batch && value) completionReveal.append(doc, batch, key, source, revealed)
doc.appendMarkdown('\n\n---\n\n')
}
return doc
}

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))
Expand All @@ -51,6 +54,7 @@ function valueHover (key, document, range) {
}

function hover (language, document, position) {
if (!settings.secretpeekingEnabled(document.uri)) return undefined
const regexDict = {
javascript: /(?:process|import\.meta)\.env\.([A-Z]{1}[A-Z_0123456789]+)/,
ruby: /ENV\[['"]([A-Z]{1}[A-Z_0123456789]+)['"]\]/,
Expand Down
3 changes: 3 additions & 0 deletions lib/hover-reveal.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const vscode = require('vscode')
const crypto = require('crypto')
const settings = require('./settings')
const command = 'dotenv.toggleHoverValue'
const links = new Map()
let pending
Expand Down Expand Up @@ -34,12 +35,14 @@ function run (context) {
const editor = vscode.window.activeTextEditor
if (running || !request || request.expires < Date.now() || !editor || !matches(request, editor.document, request.key, request.range)) return
links.delete(id)
if (!settings.secretpeekingEnabled(editor.document.uri)) return
running = true
try {
await vscode.commands.executeCommand('editor.action.hideHover')
if (vscode.window.activeTextEditor !== editor || !matches(request, editor.document, request.key, request.range)) return
// VS Code reopens hovers at the cursor, which may differ from the mouse position.
editor.selection = new vscode.Selection(request.range.start, request.range.start)
if (!settings.secretpeekingEnabled(editor.document.uri)) return
pending = request
await vscode.commands.executeCommand('editor.action.showHover', { focus: 'autoFocusImmediately' })
} finally {
Expand Down
14 changes: 13 additions & 1 deletion test/suite/lib/completion-reveal.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ function fixture () {
})
return module.exports
}
const reveal = load('completion-reveal.js', { vscode: fakeVscode })
const settings = { cloakIcon: () => '█', secretpeekingEnabled: () => true }
const reveal = load('completion-reveal.js', { vscode: fakeVscode, './settings': settings })
const helpers = load('helpers.js', {
vscode: fakeVscode,
'./completion-reveal': reveal,
Expand Down Expand Up @@ -69,6 +69,18 @@ function fixture () {
}

describe('completion popup reveal', () => {
it('keeps autocomplete suggestions but removes expanded details when peeking is disabled', async () => {
const f = fixture()
const token = f.tokens(f.original[0])[0]
f.settings.secretpeekingEnabled = () => false
const items = f.helpers.autocomplete('.', f.editor.document, f.editor.selection.active)
assert(items.length > 0)
assert.strictEqual(items[0].label.label, 'HELLO')
assert.strictEqual(items[0].insertText, '.HELLO')
assert.strictEqual(items[0].documentation, undefined)
assert.strictEqual(await f.click(token), undefined)
assert.strictEqual(f.triggers, 0)
})
it('reveals one source only, preserves insertion, and masks again on hide and fresh requests', async () => {
const f = fixture()
const original = f.original[0]
Expand Down
9 changes: 6 additions & 3 deletions test/suite/lib/dotnet.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,19 +68,22 @@ describe('.NET hover', () => {
assert.strictEqual(dotnet.hover.provideHover(document(text), new vscode.Position(0, text.indexOf('UNKNOWN'))).contents[0], settings.missingText())
})

it('handles lowercase keys, empty values, and short masked values', () => {
it('handles lowercase keys, empty values, and disabled peeking', () => {
const originalParse = helpers.envValues
const originalPeeking = settings.secretpeekingEnabled
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', '█'], ['EMPTY', '(empty)']]) {
for (const [key, expected] of [['lower_key', 'x'], ['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)
settings.secretpeekingEnabled = () => true
assert(dotnet.hover.provideHover(document(text), new vscode.Position(0, text.indexOf(key))).contents[0].value.includes(new vscode.MarkdownString().appendText(expected).value))
}
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('█████'))
assert(dotnet.hover.provideHover(document(text), new vscode.Position(0, text.indexOf('HELLO'))).contents[0].value.includes('World'))
helpers.envValues = () => new Map()
assert.strictEqual(dotnet.hover.provideHover(document(text), new vscode.Position(0, text.indexOf('HELLO'))).contents[0], settings.missingText())
} finally {
Expand Down
21 changes: 8 additions & 13 deletions test/suite/lib/env-discovery-providers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,27 +69,23 @@ describe('dotenv discovery through language providers', () => {
})
}

it('masks every conflicting value when secret peeking is disabled', async () => {
it('keeps suggestions without value details when secret peeking is disabled', async () => {
const original = settings.secretpeekingEnabled
try {
settings.secretpeekingEnabled = () => false
const uri = await write('app/src/masked.js', 'process.env.')
const document = await vscode.workspace.openTextDocument(uri)
const item = helpers.autocomplete('.', document, new vscode.Position(0, 12)).find(item => item.label.label === 'DISCOVERY_KEY')
const hover = helpers.valueHover('DISCOVERY_KEY', document).contents[0]
for (const content of [item.documentation.value, hover.value]) {
assert(!content.includes('localvalue'))
assert(!content.includes('productionvalue'))
assert(!content.replace(/\[.*?\]\(command:[^)]*\)/g, '').includes('ue'), 'Must not reveal the last two characters')
assert(content.includes('█'))
assert(content.includes('.env.local'))
}
assert.strictEqual(helpers.valueHover('DISCOVERY_KEY', document), undefined)
assert.strictEqual(item.documentation, undefined)
assert(item.label.description.includes('.env.local'))
assert.strictEqual(item.insertText, '.DISCOVERY_KEY')
} finally {
settings.secretpeekingEnabled = original
}
})

it('fully masks short and long values in completion labels, documentation and hover', async () => {
it('keeps short and long values masked in suggestions without peeking details', async () => {
const originalPeeking = settings.secretpeekingEnabled
const originalIcon = settings.cloakIcon
try {
Expand All @@ -102,9 +98,8 @@ describe('dotenv discovery through language providers', () => {
const item = helpers.autocomplete('.', document, new vscode.Position(0, 12)).find(item => item.label.label === 'MASK_TEST')
const mask = '█'.repeat(value.length)
assert.strictEqual(item.label.detail, ` ${mask}`)
assert(item.documentation.value.includes(mask))
assert(!item.documentation.value.replace(/\[.*?\]\(command:[^)]*\)/g, '').includes(value.slice(-2)))
assert(helpers.valueHover('MASK_TEST', document).contents[0].value.includes(mask))
assert.strictEqual(item.documentation, undefined)
assert.strictEqual(helpers.valueHover('MASK_TEST', document), undefined)
}
settings.secretpeekingEnabled = () => true
assert(helpers.valueHover('MASK_TEST', document).contents[0].value.includes('🌴secret'))
Expand Down
7 changes: 5 additions & 2 deletions test/suite/lib/env-literals.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,17 @@ for (const [language, provider, calls, unrelated] of [
assert.strictEqual(provider.completion.provideCompletionItems(document(text), new vscode.Position(0, text.length)), undefined)
})
}
it('handles masking, empty values, missing keys and missing files', () => {
it('handles disabled peeking, empty values, missing keys and missing files', () => {
const originalParse = helpers.envValues
const originalPeeking = settings.secretpeekingEnabled
try {
settings.secretpeekingEnabled = () => false
helpers.envValues = () => new Map(Object.entries({ lower_key: 'World', EMPTY: '' }).map(([key, value]) => [key, [{ value, source: '.env' }]]))
for (const [key, value] of [['lower_key', '█████'], ['EMPTY', '(empty)'], ['UNKNOWN', settings.missingText()]]) {
for (const [key, value] of [['lower_key', 'World'], ['EMPTY', '(empty)'], ['UNKNOWN', settings.missingText()]]) {
const text = expression(calls[0], `"${key}"`)
settings.secretpeekingEnabled = () => false
assert.strictEqual(provider.hover.provideHover(document(text), new vscode.Position(0, text.indexOf(key))), undefined)
settings.secretpeekingEnabled = () => true
const content = provider.hover.provideHover(document(text), new vscode.Position(0, text.indexOf(key))).contents[0]
assert((typeof content === 'string' ? content : content.value).includes(typeof content === 'string' ? value : new vscode.MarkdownString().appendText(value).value))
}
Expand Down
57 changes: 49 additions & 8 deletions test/suite/lib/hover-reveal.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const path = require('path')
const vm = require('vm')
const vscode = require('vscode')

function fixture (defaultRevealed = false) {
function fixture (defaultRevealed = true) {
const document = { uri: vscode.Uri.file('/test/hover.js'), version: 1 }
const range = new vscode.Range(0, 12, 0, 17)
const editor = { document, selection: new vscode.Selection(3, 0, 3, 0) }
Expand All @@ -32,8 +32,8 @@ function fixture (defaultRevealed = false) {
})
return module.exports
}
const reveal = load('hover-reveal.js', { vscode: fakeVscode })
const settings = { cloakIcon: () => '█', secretpeekingEnabled: () => defaultRevealed, missingText: () => 'MISSING' }
const reveal = load('hover-reveal.js', { vscode: fakeVscode, './settings': settings })
const helpers = load('helpers.js', {
vscode: fakeVscode,
'./hover-reveal': reveal,
Expand All @@ -56,9 +56,19 @@ function fixture (defaultRevealed = false) {
}

describe('hover popup reveal', () => {
it('returns no hover when disabled and rejects previously issued reveal links', async () => {
const f = fixture()
const masked = await f.click(f.token(f.hover()))
const token = f.token(masked)
f.settings.secretpeekingEnabled = () => false
assert.strictEqual(f.hover(), undefined)
const before = f.triggers
assert.strictEqual(await f.click(token), undefined)
assert.strictEqual(f.triggers, before)
})
it('shows and hides a masked value without changing settings or fresh hovers', async () => {
const f = fixture()
const masked = f.hover()
const masked = await f.click(f.token(f.hover()))
assert(masked.contents[0].value.startsWith('.env\n\n'))
assert(!masked.contents[0].value.includes('SECRET'))
assert(masked.contents[0].value.includes('██████'))
Expand All @@ -73,8 +83,8 @@ describe('hover popup reveal', () => {
assert(hidden.contents[0].value.startsWith('.env\n\n'))
assert(!hidden.contents[0].value.includes('SECRET'))
assert(hidden.contents[0].value.includes('██████'))
assert.strictEqual(f.settings.secretpeekingEnabled(), false)
assert(f.hover().contents[0].value.includes('██████'))
assert.strictEqual(f.settings.secretpeekingEnabled(), true)
assert(f.hover().contents[0].value.includes('SECRET'))
assert.strictEqual(hidden.contents[1].isTrusted.enabledCommands[0], 'dotenv.toggleHoverValue')
assert.strictEqual(hidden.contents[1].isTrusted.enabledCommands.length, 1)
})
Expand All @@ -96,7 +106,7 @@ describe('hover popup reveal', () => {
const content = f.hover().contents[0].value
assert(content.includes('.env\n\n'))
assert(content.includes('.env.local\n\n'))
assert(!content.includes('SECRET'))
assert(content.includes('SECRET'))
})

it('ignores unknown, reused, edited-document, and other-file links', async () => {
Expand Down Expand Up @@ -135,7 +145,7 @@ it('refreshes the real VS Code hover after clicking Reveal value and Hide value'
let provider
try {
await vscode.extensions.getExtension('dotenv.dotenv-vscode').activate()
settings.secretpeekingEnabled = () => false
settings.secretpeekingEnabled = () => true
await vscode.workspace.fs.writeFile(uri, Buffer.from('HELLO'))
const document = await vscode.workspace.openTextDocument(uri)
await vscode.window.showTextDocument(document)
Expand All @@ -147,7 +157,7 @@ it('refreshes the real VS Code hover after clicking Reveal value and Hide value'
})
const token = hover => JSON.parse(decodeURIComponent(hover.contents[1].value.match(/\?([^)]*)/)[1]))[0]
let current = helpers.valueHover('HELLO', document, range)
for (const expected of ['World', '█████']) {
for (const expected of ['█████', 'World']) {
latest = undefined
await vscode.commands.executeCommand('dotenv.toggleHoverValue', token(current))
const deadline = Date.now() + 3000
Expand All @@ -166,3 +176,34 @@ it('refreshes the real VS Code hover after clicking Reveal value and Hide value'
await vscode.workspace.fs.delete(uri)
}
})

it('disables in-code hovers when the actual secret-peeking setting is unchecked', async function () {
// Settings writes and cold language-provider startup can exceed Mocha's 2s default on CI.
this.timeout(15000)
const uri = vscode.Uri.joinPath(vscode.workspace.workspaceFolders[0].uri, 'peeking-setting.js')
const config = vscode.workspace.getConfiguration('dotenv', uri)
const original = config.inspect('enableSecretpeeking').workspaceValue
try {
await vscode.extensions.getExtension('dotenv.dotenv-vscode').activate()
await vscode.workspace.fs.writeFile(uri, Buffer.from('process.env.HELLO\n'))
await vscode.workspace.openTextDocument(uri)
for (const enabled of [true, false, true]) {
await config.update('enableSecretpeeking', enabled, vscode.ConfigurationTarget.Workspace)
const hovers = await vscode.commands.executeCommand('vscode.executeHoverProvider', uri, new vscode.Position(0, 14))
const content = hovers.flatMap(hover => hover.contents).map(item => item.value || '').join('\n')
assert.strictEqual(content.includes('.env'), enabled)
assert.strictEqual(content.includes('World'), enabled)
const completions = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', uri, new vscode.Position(0, 12))
const item = completions.items.find(item => item.label.label === 'HELLO')
assert(item, 'Autocomplete must remain available')
assert.strictEqual(!!item.documentation, enabled)
if (!enabled) {
assert(!content.includes('█████'))
assert(!content.includes('Reveal value'))
}
}
} finally {
await config.update('enableSecretpeeking', original, vscode.ConfigurationTarget.Workspace)
await vscode.workspace.fs.delete(uri)
}
})
2 changes: 1 addition & 1 deletion test/suite/lib/javascript-env.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe('named process env imports', () => {
settings.secretpeekingEnabled = () => false
const document = documentFor("import { env } from 'node:process'\nenv.HELLO")
const hover = providers.javascriptHover.provideHover(document, new vscode.Position(1, 6))
assert(hover.contents[0].value.includes('█████'))
assert.strictEqual(hover, undefined)
} finally {
settings.secretpeekingEnabled = original
}
Expand Down
Loading