Repository navigation
[Bug]: Template copy() includes files that .dockerignore / fileIgnorePatterns should exclude (JS + Python) #1902
Description
Sandbox ID or Build ID
N/A. Client-side bug, reproducible offline without building anything.
Environment
e2b(JS) 2.51.0 ande2b(Python) 2.51.0, currentmain(ccaf9fc0)- glob 13.0.6, wcmatch 11.0.1
- macOS 26.5.2, Node 24.12.0, Python 3.14.2
Timestamp of the issue
2026-09-25 UTC (not time-dependent)
Frequency
Happens every time
Expected behavior
Paths matched by .dockerignore or fileIgnorePatterns (file_ignore_patterns in Python) stay out of the file set that copy() hashes and uploads, as they do in a Docker build context:
- Patterns apply no matter how
srcis written (.,./,./src,src/.,dist/../src,src), and the context root itself is never excluded. - A pattern that matches a directory (
node_modules,.git,dist/,src/generated,**/node_modules) excludes everything under it. - A leading
/is ignored, as in Docker (/node_modulesis the same asnode_modules).
The docs say the same: files matching .dockerignore and fileIgnorePatterns "are excluded from uploads and hash calculations" (Base image). Their example, fileIgnorePatterns: [".git", "node_modules"], is hit by bug 2 below (and bug 1 in Python) and uploads both directories' contents with copy('.').
Actual behavior
The shared collector (getAllFilesInPath in JS, get_all_files_in_path in Python) has three bugs. The files hash and the upload tar both use its output.
- Python only: if
srcis.or has a.or..segment (.,./,./src,src/.,dist/../srcwith an existingdist), an ignore pattern matches only if it repeats that segment (for.,./and./src, it must start with./). So.env,node_modulesand even**/*.spec.*are skipped, andcopy('.')uploads every file they ignore. A.*pattern matches the root.itself, socopy('.')fails withNo files found. JS normalizes thesesrcforms and keeps the root. - Both SDKs: when a copied directory is walked (
copy('.'),copy('src')), a pattern that matches a subdirectory (node_modules,.git,dist/,src/generated,**/node_modules) drops only that directory entry, not its contents. A bare pattern works only whensrcitself matches the directory (copy('*'),copy('src/*')). Otherwise the contents are excluded only by a pattern that matches them directly, such asnode_modules/**. In Python, bug 1 masks this forcopy('.');copy('src')shows it. - Both SDKs: a root-relative leading
/(/node_modules,/node_modules/**) matches nothing, not even the directory itself.
Also, a . or ./ ignore line (which Docker ignores) makes every JS copy() step throw TypeError: empty pattern list when the template is built or serialized, and in Python it excludes the root, so copy('.') fails with No files found.
Impact:
- Files inside ignored directories (host
node_modules,.venv,.git, asecrets/dir) are uploaded into the template image in both SDKs. In Python,copy('.')uploads every ignored file, including.env(unless the pattern is written./.env). Anyone who can start a sandbox from the template can read them: the team, or everyone once it is made public withe2b template publish. fromDockerfile()(ande2b template create) turnsCOPY . /appintocopy(), so Dockerfile users with a typical.dockerignoreare affected too.- Ignored files under
.git(index, refs, logs) are part of the files hash. Every commit changes thefilesHashofcopy('.'), so the SDK re-uploads the context and the build cache misses from that step on. Later steps (for example package installs after the copy) re-run.
Issue reproduction
Run everything from the root of an e2b-dev/E2B checkout (after pnpm install). Build context:
mkdir -p ctx/node_modules/pkg ctx/src/generated ctx/.git && cd ctx
printf '.env\nnode_modules\n.git\n**/*.spec.*\nsrc/generated\n' > .dockerignore
echo SECRET=1 > .env
echo x > node_modules/pkg/index.js
echo x > src/app.ts
echo x > src/app.spec.ts
echo x > src/generated/api.ts
echo ref > .git/HEAD
cd ..Python (save as repro.py, run uv run --project packages/python-sdk python repro.py):
import json, os
from e2b import Template
from e2b.template.utils import get_all_files_in_path, read_dockerignore
ctx = os.path.abspath("ctx")
ignore = read_dockerignore(ctx)
for src in [".", "./src", "src"]:
files = get_all_files_in_path(src, ctx, ignore, True)
print(f"{src!r:8}", sorted(os.path.relpath(f, ctx) for f in files))
def files_hash():
t = Template(file_context_path=ctx).from_image("node:22").copy(".", "/app")
return json.loads(Template.to_json(t))["steps"][0]["filesHash"]
before = files_hash()
open(os.path.join(ctx, ".git/HEAD"), "a").write("x")
print("filesHash changed after editing ignored .git/HEAD:", before != files_hash())'.' ['.', '.dockerignore', '.env', '.git', '.git/HEAD', 'node_modules', 'node_modules/pkg', 'node_modules/pkg/index.js', 'src', 'src/app.spec.ts', 'src/app.ts', 'src/generated', 'src/generated/api.ts']
'./src' ['src', 'src/app.spec.ts', 'src/app.ts', 'src/generated', 'src/generated/api.ts']
'src' ['src', 'src/app.ts', 'src/generated/api.ts']
filesHash changed after editing ignored .git/HEAD: True
JS (save as repro.mts, run pnpm dlx tsx repro.mts):
import * as fs from 'node:fs'
import * as path from 'node:path'
import { Template } from './packages/js-sdk/src/template'
// internal helpers
import { getAllFilesInPath, readDockerignore } from './packages/js-sdk/src/template/utils'
const ctx = path.resolve('ctx')
const ignore = readDockerignore(ctx)
for (const src of ['.', './src', 'src']) {
const files = await getAllFilesInPath(src, ctx, ignore, true)
console.log(JSON.stringify(src).padEnd(8), JSON.stringify(files.map((f) => path.relative(ctx, f.fullpath()) || '.').sort()))
}
const filesHash = async () => {
const t = Template({ fileContextPath: ctx }).fromImage('node:22').copy('.', '/app')
return JSON.parse(await Template.toJSON(t)).steps[0].filesHash
}
const before = await filesHash()
fs.appendFileSync(path.join(ctx, '.git/HEAD'), 'x')
console.log('filesHash changed after editing ignored .git/HEAD:', before !== (await filesHash()))"." [".",".dockerignore",".git/HEAD","node_modules/pkg","node_modules/pkg/index.js","src","src/app.ts","src/generated/api.ts"]
"./src" ["src","src/app.ts","src/generated/api.ts"]
"src" ["src","src/app.ts","src/generated/api.ts"]
filesHash changed after editing ignored .git/HEAD: true
JS ("." row: .git, node_modules, src/generated) and Python ('src' row: src/generated) show bug 2: files under those directories are included while the ignored directory entries are dropped. Python's '.' and './src' rows show bug 1 (compare './src' with 'src'): no pattern applies, so .env, src/app.spec.ts and the directory entries are included too. For copy('.'), the upload tar (spoolTarArchive / tar_file_stream) contains exactly the '.' row.
Additional context
Root cause
packages/python-sdk/e2b/template/utils.py:156-176: wcmatch fullmatchesexcludepatterns against the raw result, which keeps every.and..segment fromsrc(./src,src/.,dist/../src) or from the recursion base (normalize_path(file) + "/**/*"is./**/*for.)..envnever matches./.env, and even**/*.spec.*doesn't match./src/app.spec.ts. The root result.is tested as well, so.*excludes it. JS is unaffected because node-glob testsignoreagainst each result's normalized relative path.packages/js-sdk/src/template/utils.ts:148-173and the same Python function:.dockerignorelines (utils.ts:109-120,utils.py:105-124) go unchanged into node-globignore/ wcmatchexclude, which test each result on its own. node-glob skips a directory's children only for patterns whose last segment is**. A leading/makes node-glob treat the pattern as absolute and match it against the full host path, and wcmatch compares it with the relative result, so a root-relative pattern like/node_modulesnever matches (bug 3). (In JS, a/**/prefix does match, since it spans the host path.) Docker cleans each pattern (drops./and a trailing/, strips a leading/) and excludes everything under a matched path.- The merged patterns feed both the hash and the upload:
packages/js-sdk/src/template/index.ts:1135-1138, 1226-1231andpackages/python-sdk/e2b/template/main.py:1251-1254,template_sync/main.py:134-137,template_async/main.py:134-137.
Proposed fix (happy to send a PR)
- Normalize ignore patterns inside the collector as Docker does: skip empty lines, clean each pattern and strip a leading
/asignorefile.ReadAlldoes (drop./and a trailing/, resolve.., strip a UTF-8 BOM, which Python'sread_dockerignorekeeps today so a BOM file's first line never matches), and ignore a bare.pattern (Docker docs: "For historical reasons, the pattern.is ignored"). Exception: JS matches an absolute pattern (from either source) against the full host path today (<ctx>/secrets/**excludessecrets/; Python matches nothing). Stripping its/would silently start uploading those files, so first rewrite an absolute pattern that starts with the literal context path relative to it (in both SDKs), then strip a leading/from the rest. (For a.dockerignoreline this differs from Docker, which reads/app/cachein context/appasapp/cache; it only affects absolute paths into the context.) Then match each pattern against the path and its parent directories, never the context root, as Docker'sMatchesOrParentMatchesdoes, and don't descend into excluded directories (node-glob prunesdir/**patterns today; a post-filter would walk a hugenode_modules; wcmatch'sexcludenever prunes, so Python needs its own walk). One change per SDK fixes both the hash and the upload tar. The CLI builds through the JS SDK, so it is covered too. - Python: normalize
.and..segments insrcwithposixpath.normpath(keeping an emptysrcand a trailing/as they are), so results are root-relative as in JS. The pruning walk above replaces thenormalize_path(file) + "/**/*"recursion glob. - Tests (offline):
packages/js-sdk/tests/template/utils/getAllFilesInPath.test.tsandspoolTarArchive.test.ts, mirrored inpackages/python-sdk/tests/shared/template/utils/. Cover.,./,./src,src/.,dist/../src, bare directory patterns, copying a path inside an ignored directory, a leading/,.*and.(must not drop the root or throw), an absolutefileIgnorePatternsentry under the context,node_modulesnot excludingsrc/node_modules(patterns stay root-anchored) and a hash-stability check for an ignored.git/HEAD. - Behavior changes (I'd call them out in the changeset):
- Files inside ignored directories stop being uploaded, as in Docker, and the files hash changes once for affected copies. A template that relied on an ignored directory's contents (for example a host
node_moduleslisted in.dockerignore) must drop that ignore line. - Copying a path inside an ignored directory by name (
.nextignored, thencopy('.next/standalone', ...)) works today in both SDKs and would fail withNo files found, as Docker'sCOPYfails when the path is not in the context. Without!support, the workaround is narrowing or dropping the ignore line, and dropping it re-includes the whole directory forcopy('.'). - Python: patterns start applying when
srchas.or..segments, socopy('.')stops uploading.envand the other ignored files, andmissing/../srcstops failing withNo files found. - Leading-
/patterns start excluding in both SDKs, and absolutefile_ignore_patternsunder the context start excluding in Python. fileIgnorePatternsgets the same rules, since it is merged with the.dockerignorelines.
- Files inside ignored directories stop being uploaded, as in Docker, and the files hash changes once for affected copies. A template that relied on an ignored directory's contents (for example a host
Question for maintainers: ! negation
Neither SDK supports ! lines today (node-glob compiles ignore with nonegate, and wcmatch runs without NEGATE). So an allowlist .dockerignore like
*
!src
is already broken: copy('src') fails with "No files found" in both SDKs, and copy('.') uploads the contents of every top-level directory (node_modules/**, .git/**, ...) in JS and everything in Python. Docker's context has only src and its contents. The directory fix alone would make copy('.') upload only an empty root. It would also drop a file re-included with !node_modules/foo/index.js, which is uploaded today with a bare node_modules line and copy('.'), and it would leave no way to copy a path inside an ignored directory (see Behavior changes). Which do you prefer?
- Implement Docker's rule in the same PR: the last matching pattern wins, each pattern is checked against the path and its parents, and excluded directories are still walked when a later
!pattern could match inside them (a small matcher in each SDK).fileIgnorePatternscome before the.dockerignorelines in the merged list, so under last-match-wins a.dockerignore!line could re-include a path excluded byfileIgnorePatterns. KeepingfileIgnorePatternsas a separate always-exclude list avoids that. - Ship the directory fix first and log a warning when a
!line is present. An error would break builds that work today.
Related
- Fix ignore files in file upload and added tests #1012 wired ignore patterns into uploads. fix: include dotfiles in template file uploads #1162 made the collector include dotfiles, so ignore patterns are all that keeps
.envand.gitout. - Fixes dot copy #1037 fixed JS
copy('.')walking/(Building a template that requires running commands like copy, fails #1036), not ignore matching. - Simplify getAllFilesInPath with single glob call #1107 and refactor(sdk): simplify getAllFilesInPath to a single glob call #1475 (closed, unmerged) proposed a single-glob rewrite of the same function. A bot review on refactor(sdk): simplify getAllFilesInPath to a single glob call #1475 noted it would stop bare directory patterns from pruning even when
srcmatches the directory. - No open issue or PR covers this.
Activity
hardness1020 commentedon Sep 26, 2026
I'd like to work on this and will open a PR with the proposed fix (both SDKs, with tests and a changeset). Input on the
!negation question above is welcome before then.devin-ai-integration commentedon Sep 26, 2026
Triage
getAllFilesInPath) + python-sdk/template utils (get_all_files_in_path).dockerignore/fileIgnorePatternsare not applied Docker-style in both SDKs' templatecopy()file collector (directory patterns don't exclude contents, leading/matches nothing, Python skips patterns whensrchas./..segments), causing ignored files such as.env,node_modulesand.gitto be uploaded into templates and to churn the files hash; the reporter has volunteered a PR and asks maintainers to decide whether!negation support should land in the same PR or as a follow-up.Written by Devin
fix(sdk): apply .dockerignore patterns Docker-style when copying temp…
devin-ai-integration commentedon Oct 1, 2026
@hardness1020 thanks for the detailed report and repro! The fix shipped: template
copy()now applies.dockerignore/fileIgnorePatternswith Docker-style semantics (directory patterns, leading/, negations,./..sources) consistently for hashing and upload in both SDKs — #1917It's available in JS SDK
e2b@2.52.0and Python SDKe2b==2.52.0: