{"record":{"id":"a40526a5a20d8b71","repo":"yamadashy/repomix","slug":"refusing-to-trust-configname-it-resolves-outsi","errorCode":null,"errorMessage":"Refusing to trust ${configName}: it resolves outside the cloned repository.","messagePattern":"Refusing to trust (.+?): it resolves outside the cloned repository\\.","errorType":"exception","errorClass":"RepomixError","httpStatus":null,"severity":"error","filePath":"src/cli/prompts/remoteConfigTrustPrompt.ts","lineNumber":120,"sourceCode":" * show the user must live in the tree we just cloned.\n */\nconst assertConfigIsContained = async (\n  configPath: string,\n  repoDir: string,\n  deps: Pick<ConfirmRemoteConfigTrustDeps, 'lstat' | 'realpath'>,\n): Promise<void> => {\n  const configName = path.basename(configPath);\n  const stats = await deps.lstat(configPath);\n  if (stats.isSymbolicLink() || !stats.isFile()) {\n    throw new RepomixError(\n      `Refusing to trust ${configName}: the remote repository's config must be a regular file, not a symlink.`,\n    );\n  }\n\n  const [realConfigPath, realRepoDir] = await Promise.all([deps.realpath(configPath), deps.realpath(repoDir)]);\n  const relative = path.relative(realRepoDir, realConfigPath);\n  if (relative.startsWith('..') || path.isAbsolute(relative)) {\n    throw new RepomixError(`Refusing to trust ${configName}: it resolves outside the cloned repository.`);\n  }\n};\n\n/**\n * Interactively confirm before a cloned remote repository's config is trusted\n * (via `--remote-trust-config` / `REPOMIX_REMOTE_TRUST_CONFIG`). Shows the config\n * that is about to run, then asks the user. Throws `OperationCancelledError` when\n * the user declines.\n *\n * Proceeds without prompting when: `--force` is passed, the shell is\n * non-interactive (CI/pipes — preserves the historical behavior so automations do\n * not hang), the remote's exact config is already trusted, an absolute `--config`\n * is in use, or the cloned repo has no config to load.\n */\nexport const confirmRemoteConfigTrust = async (\n  options: ConfirmRemoteConfigTrustOptions,\n  deps: ConfirmRemoteConfigTrustDeps = {\n    findLocalConfigPath,","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/yamadashy/repomix/blob/f465ad909315a22120636baf03fa5e28701a50cb/src/cli/prompts/remoteConfigTrustPrompt.ts#L102-L138","documentation":"After confirming the config is a regular file, assertConfigIsContained resolves both the config and the clone directory with fs.realpath and computes path.relative. If the config's real path is not inside the clone's real path (relative path starts with '..' or is absolute), trust is refused. This blocks containment escapes such as a regular-file config inside a symlinked subdirectory pointing out of the temp clone dir, and like error 30 it applies even with --force and in CI.","triggerScenarios":"`repomix --remote <url>` where the config file's realpath resolves outside the cloned repository's realpath — e.g. the config sits in a directory that is a symlink to somewhere outside the clone, or an OS-level temp-dir alias makes realpath disagree. Thrown from confirmRemoteConfigTrust via assertConfigIsContained.","commonSituations":"A repo commits a symlinked directory (e.g. config -> ../../shared) containing repomix.config.json; a bind-mount or symlinked /tmp on macOS (/tmp -> /private/tmp) combined with inconsistent path inputs; exotic container mounts where the clone dir itself resolves oddly.","solutions":["Restructure the remote repo so the config physically lives inside the repository tree with no symlinked ancestor directories.","Clone/inspect the repo and run `realpath` on the config's parent directories to find which link escapes the tree, then replace it with a real directory.","If your environment uses symlinked temp dirs, ensure repoDir and the config are referenced consistently (usually a repo-layout problem, not an env problem).","Bypass the remote repo's config entirely by passing `--config /absolute/path/to/your.config.json`."],"exampleFix":"# before (in the remote repo)\nconfig/ -> /shared/config   (symlinked dir)\nconfig/repomix.config.json\n\n# after\nrm config && mkdir config\ncp /shared/config/repomix.config.json config/repomix.config.json\ngit add config && git commit -m \"un symlink config dir\"","handlingStrategy":"validation","validationCode":"import fs from 'node:fs/promises';\nimport path from 'node:path';\nconst [realConfig, realRepo] = await Promise.all([fs.realpath(configPath), fs.realpath(repoDir)]);\nconst rel = path.relative(realRepo, realConfig);\nif (rel.startsWith('..') || path.isAbsolute(rel)) {\n  throw new Error(`${configPath} resolves outside ${repoDir}`);\n}","typeGuard":"const isContained = (realConfigPath: string, realRepoDir: string): boolean => {\n  const rel = path.relative(realRepoDir, realConfigPath);\n  return !rel.startsWith('..') && !path.isAbsolute(rel);\n};","tryCatchPattern":"try {\n  await runRemotePack();\n} catch (e) {\n  if (e instanceof RepomixError && e.message.includes('resolves outside the cloned repository')) {\n    console.error('Remote config escapes the clone dir; restructure the repo or use --config.');\n  } else throw e;\n}","preventionTips":["Avoid committing symlinked directories that contain config files.","Keep configs physically inside the repository tree.","Be aware of OS-level symlinked temp dirs (/tmp -> /private/tmp) when reasoning about realpath.","Use an explicit --config to bypass remote config resolution entirely."],"tags":["security","path-traversal","remote-config","filesystem"],"backgroundTag":"path-outside-base-directory","analyzedSha":"f465ad909315a22120636baf03fa5e28701a50cb","analyzedAt":"2026-08-29T01:27:42.024Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}