Skip to content
Open
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Changed
- **Bazel diagnostics** — `socket manifest bazel --verbose` now emits bounded subprocess traces with argv, cwd, duration, exit status, output sizes, and failure stderr tails to make customer log-only triage safer and faster.

## [1.1.112](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.112) - 2026-05-29

### Fixed
- `socket fix` and `socket scan create` no longer abort with `EACCES: permission denied, scandir` when the project contains a directory the running user cannot read (for example a postgres `pgdata` data directory owned by another uid, or a Docker volume mount). Manifest discovery walks a project for `.gitignore` files before applying any path exclusions; that walk now honors `--exclude-paths` and `socket.yml` `projectIgnorePaths`, and skips unreadable directories rather than crashing. This makes `--exclude-paths` effective for unreadable directories — previously the crash happened before the exclusion was ever applied.

## [1.1.111](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.111) - 2026-05-29

### Changed
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "socket",
"version": "1.1.111",
"version": "1.1.112",
"description": "CLI for Socket.dev",
"homepage": "https://github.com/SocketDev/socket-cli",
"license": "MIT AND OFL-1.1",
Expand Down
59 changes: 42 additions & 17 deletions src/utils/glob.mts
Original file line number Diff line number Diff line change
Expand Up @@ -232,23 +232,48 @@ export async function globWithGitIgnore(

const ignores = new Set<string>(IGNORED_DIR_PATTERNS)

// CLI-supplied `additionalIgnores` are already anchored minimatch — they
// must not pass through the `ignore` package (whose gitignore "match
// anywhere" semantics would re-interpret a bare `tests` to match
// `subdir/tests/foo.json`). Keep them in fast-glob's ignore list across
// both paths; only gitignore-translated entries go into the `ig` matcher.
const cliMinimatchIgnores = additionalIgnores ?? []

const projectIgnorePaths = socketConfig?.projectIgnorePaths
if (Array.isArray(projectIgnorePaths)) {
const ignorePatterns = ignoreFileLinesToGlobPatterns(
projectIgnorePaths,
path.join(cwd, '.gitignore'),
cwd,
)
for (const pattern of ignorePatterns) {
ignores.add(pattern)
}
const projectIgnoreGlobs = Array.isArray(projectIgnorePaths)
? ignoreFileLinesToGlobPatterns(
projectIgnorePaths,
path.join(cwd, '.gitignore'),
cwd,
)
: []
for (const pattern of projectIgnoreGlobs) {
ignores.add(pattern)
}

// The .gitignore discovery walk has to honor the same directory exclusions
// as the package walk below. Otherwise an unreadable subtree (e.g. a
// postgres `pgdata` dir owned by another uid, or a Docker volume mount) makes
// fast-glob throw `EACCES: permission denied, scandir` *here* — before
// --exclude-paths (`cliMinimatchIgnores`) or projectIgnorePaths are ever
// applied to the main walk, which is why excluding the path did not help.
// `suppressErrors` is the backstop: a directory the user simply cannot read
// cannot contain manifests they could scan anyway, so skip it instead of
// aborting the whole `socket fix` / `socket scan` run. Negated patterns are
// dropped — for a discovery walk they could only re-include a subtree (never
// prevent a crash), and fast-glob treats `!` ignore entries inconsistently.
const gitIgnoreStream = fastGlob.globStream(['**/.gitignore'], {
absolute: true,
cwd,
dot: true,
ignore: DEFAULT_IGNORE_FOR_GIT_IGNORE,
ignore: [
...DEFAULT_IGNORE_FOR_GIT_IGNORE,
...projectIgnoreGlobs,
...cliMinimatchIgnores,
]
.filter(p => p.charCodeAt(0) !== 33 /*'!'*/)
.map(stripTrailingSlash),
suppressErrors: true,
})
for await (const ignorePatterns of transform(
gitIgnoreStream,
Expand All @@ -273,13 +298,6 @@ export async function globWithGitIgnore(
}
}

// CLI-supplied `additionalIgnores` are already anchored minimatch — they
// must not pass through the `ignore` package (whose gitignore "match
// anywhere" semantics would re-interpret a bare `tests` to match
// `subdir/tests/foo.json`). Keep them in fast-glob's ignore list across
// both paths; only gitignore-translated entries go into the `ig` matcher.
const cliMinimatchIgnores = additionalIgnores ?? []

const globOptions = {
__proto__: null,
absolute: true,
Expand All @@ -289,6 +307,13 @@ export async function globWithGitIgnore(
? [...defaultIgnore, ...cliMinimatchIgnores]
: [...ignores, ...cliMinimatchIgnores].map(stripTrailingSlash),
...additionalOptions,
// Skip directories the running user cannot read rather than aborting the
// whole walk on the first `EACCES` (see the .gitignore discovery walk
// above for the full rationale). Pinned after `...additionalOptions` so a
// caller's options bag cannot accidentally flip it back to `false` and
// re-introduce the crash — `suppressErrors` is a safety invariant here, not
// a tunable.
suppressErrors: true,
Comment thread
mtorp marked this conversation as resolved.
} as GlobOptions

// When no filter is provided and no negated patterns exist, use the fast path.
Expand Down
54 changes: 53 additions & 1 deletion src/utils/glob.test.mts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { existsSync, readdirSync, rmSync } from 'node:fs'
import {
chmodSync,
existsSync,
mkdirSync,
mkdtempSync,
readdirSync,
rmSync,
writeFileSync,
} from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

Expand Down Expand Up @@ -269,6 +278,49 @@ describe('glob utilities', () => {
`${mockFixturePath}/package.json`,
])
})

// Reproduces the reported `socket fix` crash: a project containing a
// directory the running user cannot enter (e.g. a postgres `pgdata` dir
// owned by another uid, mode drwx------) made fast-glob throw
// `EACCES: permission denied, scandir` during manifest discovery. Uses the
// real filesystem because mock-fs only enforces permissions for non-root
// uids; skipped under root (perm checks bypassed) and on Windows (no POSIX
// directory perms).
const skipUnreadableDirTest =
process.platform === 'win32' ||
(typeof process.getuid === 'function' && process.getuid() === 0)
it.skipIf(skipUnreadableDirTest)(
'skips an unreadable directory instead of throwing EACCES',
async () => {
const realTmp = mkdtempSync(path.join(tmpdir(), 'socket-glob-perm-'))
const unreadable = path.join(realTmp, 'data/postgres/pgdata')
try {
mkdirSync(unreadable, { recursive: true })
writeFileSync(path.join(realTmp, 'package.json'), '{}')
// Files inside the directory must never surface — the user cannot
// read them, so they cannot be scanned.
writeFileSync(path.join(unreadable, 'PG_VERSION'), '17')
// drwx------ : owner-only, and the running test user is not the owner
// in the field report; locally dropping all bits has the same effect
// of making scandir fail for the current user.
chmodSync(unreadable, 0o000)

const results = await globWithGitIgnore(['**/*'], {
cwd: realTmp,
})

expect(results.map(normalizePath)).toEqual([
normalizePath(path.join(realTmp, 'package.json')),
])
} finally {
// Restore perms so recursive cleanup can descend into the locked dir.
try {
chmodSync(unreadable, 0o755)
} catch {}
rmSync(realTmp, { force: true, recursive: true })
}
},
)
})

describe('createSupportedFilesFilter()', () => {
Expand Down
Loading