From b3186015a564729ff7131b0ad23d63140dddd32e Mon Sep 17 00:00:00 2001 From: Hasan TASKIN Date: Fri, 28 Aug 2026 00:42:21 +0200 Subject: [PATCH] fix: materialize the native claude binary in the cage image --- CHANGELOG.md | 6 ++++++ package.json | 2 +- packages/cli/package.json | 2 +- packages/cli/src/task-isolation.test.ts | 3 +++ packages/cli/src/task-isolation.ts | 7 ++++++- 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8bcf47..a49cee6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to `codesema` (the npm package in `packages/cli`) are documented here. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [SemVer](https://semver.org). +## [0.18.2] - 2026-08-28 + +### Fixed + +- **The caged agent could not execute claude when the cage image was built through the native installer.** claude.ai/install.sh drops a symlink in ~/.local/bin pointing into ~/.local/share/claude; the image build runs as root, so moving that symlink to /usr/local/bin left its target under /root (mode 0700) and every caged turn died with "claude: Permission denied" for the non-root uid the turn runs as. The recipe now materializes the real binary with cp -L, chmods it 755 and removes the root-owned source tree in the same layer. First caught on the first production server's first ticket. + ## [0.18.1] - 2026-08-27 ### Fixed diff --git a/package.json b/package.json index 2222d54..68f1016 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codesema-tools", - "version": "0.18.1", + "version": "0.18.2", "private": true, "type": "module", "workspaces": [ diff --git a/packages/cli/package.json b/packages/cli/package.json index 9950e09..274f1ce 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "codesema", - "version": "0.18.1", + "version": "0.18.2", "description": "Local merge request review, step by step. Your AI agent reviews, codesema displays.", "license": "MIT", "author": "Hasan TASKIN", diff --git a/packages/cli/src/task-isolation.test.ts b/packages/cli/src/task-isolation.test.ts index 18f297b..22f766a 100644 --- a/packages/cli/src/task-isolation.test.ts +++ b/packages/cli/src/task-isolation.test.ts @@ -538,6 +538,9 @@ describe('installCommandFor', () => { expect(claude).toContain('$HOME/.local/bin/claude') expect(claude).toContain('command -v claude') expect(claude).toContain('/usr/local/bin/claude') + expect(claude).toContain('cp -L "$HOME/.local/bin/claude" /usr/local/bin/claude') + expect(claude).toContain('chmod 755 /usr/local/bin/claude') + expect(claude).not.toContain('mv "$HOME/.local/bin/claude"') const opencode = installCommandFor('opencode') expect(opencode).toContain('https://opencode.ai/install') expect(opencode).toContain('npm cache clean --force') diff --git a/packages/cli/src/task-isolation.ts b/packages/cli/src/task-isolation.ts index 1f758c6..20c2fb0 100644 --- a/packages/cli/src/task-isolation.ts +++ b/packages/cli/src/task-isolation.ts @@ -651,7 +651,12 @@ export function installCommandFor(agent: string, baseRef?: string): string { 'if [ -x "$HOME/.local/bin/opencode" ]; then mv "$HOME/.local/bin/opencode" /usr/local/bin/opencode; fi', ] : [ - 'if [ -x "$HOME/.local/bin/claude" ]; then mv "$HOME/.local/bin/claude" /usr/local/bin/claude; fi', + // The native installer drops a SYMLINK into ~/.local/bin pointing at + // ~/.local/share/claude/versions/. The build runs as root, so a + // moved symlink keeps its target under /root (0700) and the caged + // uid gets EACCES at run time. cp -L materializes the real binary; + // the source tree is removed in the same RUN so the layer holds one copy. + 'if [ -x "$HOME/.local/bin/claude" ]; then cp -L "$HOME/.local/bin/claude" /usr/local/bin/claude && chmod 755 /usr/local/bin/claude && rm -rf "$HOME/.local/share/claude" "$HOME/.local/bin/claude"; fi', ] return [ 'if command -v curl >/dev/null 2>&1 && command -v tar >/dev/null 2>&1 && command -v bash >/dev/null 2>&1; then',