{"record":{"id":"cb5d35915c827395","repo":"NousResearch/hermes-agent","slug":"unsafe-token-directory","errorCode":null,"errorMessage":"unsafe token directory","messagePattern":"unsafe token directory","errorType":"exception","errorClass":"SystemExit","httpStatus":null,"severity":"critical","filePath":"apps/desktop/electron/remote-lifecycle.ts","lineNumber":536,"sourceCode":"    throw err\n  }\n\n  const spawnNonce = crypto.randomBytes(8).toString('hex')\n  const tokenDir = ownershipDirectory(ownershipId)\n  const tokenFilePath = `${tokenDir}/${spawnNonce}.token`\n  const logPath = spawnLogPath(ownershipId, spawnNonce)\n\n  const tokenUploadPy =\n    'import os,sys,stat\\n' +\n    `p=os.path.expanduser(${shq(tokenFilePath)})\\n` +\n    'd=os.path.dirname(p)\\n' +\n    'n=os.path.basename(p)\\n' +\n    'os.makedirs(d,mode=0o700,exist_ok=True)\\n' +\n    'df=os.O_RDONLY|getattr(os,\"O_DIRECTORY\",0)|getattr(os,\"O_NOFOLLOW\",0)\\n' +\n    'dd=os.open(d,df)\\n' +\n    'try:\\n' +\n    ' s=os.fstat(dd)\\n' +\n    ' if not stat.S_ISDIR(s.st_mode):raise SystemExit(\"unsafe token directory\")\\n' +\n    ' if hasattr(os,\"getuid\") and s.st_uid!=os.getuid():raise SystemExit(\"token directory owner mismatch\")\\n' +\n    ' if (s.st_mode&0o777)!=0o700:os.fchmod(dd,0o700)\\n' +\n    ' fl=os.O_WRONLY|os.O_CREAT|os.O_EXCL|getattr(os,\"O_NOFOLLOW\",0)\\n' +\n    ' now=__import__(\"time\").time()\\n' +\n    ' for stale in os.listdir(dd):\\n' +\n    '  if stale.endswith(\".token\") and len(stale)==22:\\n' +\n    '   try:\\n' +\n    '    ss=os.stat(stale,dir_fd=dd,follow_symlinks=False)\\n' +\n    '    if stat.S_ISREG(ss.st_mode) and now-ss.st_mtime>3600:os.unlink(stale,dir_fd=dd)\\n' +\n    '   except OSError:pass\\n' +\n    ' fd=os.open(n,fl,0o600,dir_fd=dd)\\n' +\n    ' try:os.write(fd,sys.stdin.buffer.read())\\n' +\n    ' except BaseException:\\n' +\n    '  try:os.unlink(n,dir_fd=dd)\\n' +\n    '  except OSError:pass\\n' +\n    '  raise\\n' +\n    ' finally:os.close(fd)\\n' +\n    'finally:os.close(dd)'","sourceCodeStart":518,"sourceCodeEnd":554,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/apps/desktop/electron/remote-lifecycle.ts#L518-L554","documentation":"SystemExit('unsafe token directory') from the embedded Python token-upload script in apps/desktop/electron/remote-lifecycle.ts:536. The script opens the token file's parent directory with O_NOFOLLOW|O_DIRECTORY (best-effort via getattr) and then fstat's the fd; if the fd is not a real directory (symlink traversal when O_NOFOLLOW/O_DIRECTORY are unavailable, or a race replaced the entry), it aborts before writing the auth token. This is a hard security guard on where the remote-backend session token may land.","triggerScenarios":"The computed token directory path (or a component of it) is a symlink and the platform lacks O_NOFOLLOW/O_DIRECTORY enforcement; the path exists but is a file, not a directory; a hostile or misconfigured HOME/XDG layout redirects the token dir.","commonSituations":"Users with symlinked home/config dirs (dotfiles managers, macOS data-volume symlinks); containers or NFS mounts where O_NOFOLLOW is unsupported; a tampered or manually created file occupying the directory path; platform where getattr(os, 'O_DIRECTORY', 0) returned 0 so the open() checks degrade.","solutions":["Make the token directory a real directory owned by the current user with no symlinks on the path: replace the symlink with a bind mount or move the real directory.","Verify manually: `ls -ld <dir>` must show a directory (not 'l') and your uid as owner; `python -c \"import os,stat;s=os.stat('<dir>');print(stat.S_ISDIR(s.st_mode), s.st_uid)\"`.","Point the remote backend's HOME/hermes home to a real directory instead of a symlinked one.","Never bypass the check by pre-creating the token file yourself — the guard exists to keep the session token out of attacker-writable locations."],"exampleFix":"# shell: before — dir is a symlink, upload aborts\n$ ls -ld ~/.hermes/remote\ndrwx------ ... -> /mnt/other/tokens   # 'lrwxrwxrwx' actually\n\n# after — replace symlink with a real dir owned by you\n$ rm ~/.hermes/remote && mkdir -m 700 ~/.hermes/remote","handlingStrategy":"validation","validationCode":"import os, stat\n\ndef token_dir_is_safe(path: str) -> bool:\n    p = os.path.realpath(path)\n    s = os.stat(p, follow_symlinks=False)\n    return (\n        stat.S_ISDIR(s.st_mode)\n        and (not hasattr(os, \"getuid\") or s.st_uid == os.getuid())\n        and (s.st_mode & 0o777) == 0o700\n    )","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never place the token directory behind a symlink; use real directories.","Set the directory to mode 0700 owned by your uid before starting the remote backend.","Do not pre-create or hand-edit token files — the uploader enforces O_EXCL creation."],"tags":["security","electron","auth-token","filesystem","symlink"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}