feat(agents): Run as moatuser with --dangerously-skip-permissions as default#42
Conversation
High priority fixes: - SSH socket validation: Verify socat process is running and socket was created before proceeding, with warning messages on failure - UID 1000 conflict: Delete existing user with UID 1000 before creating moatuser to avoid conflicts with base images Medium priority fixes: - hasDependency(): Add length check to ensure version exists after @ - ExtractHost(): Fix IPv6 literal parsing (e.g., git@[::1]:repo.git) - MarketplacePath(): Allow "foo..bar" as valid filename, only reject ".." as complete path traversal (since / and \ are already rejected) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Adds a streamlined command for running Claude Code in isolated containers: - `moat claude` - Start Claude Code interactively in current directory - `moat claude -p "prompt"` - Run with a prompt (non-interactive) - `moat claude --resume` - Resume a previous session - `moat claude sessions` - List Claude sessions Key features: - Dual authentication: OAuth (Pro/Max subscription) or API key - Session management for tracking and resuming conversations - Automatic node@20 and git dependencies - Shared execution flags with `moat run` (--rebuild, --keep, --env, etc.) Architecture: - New exec.go with shared ExecFlags, ExecOptions, and ExecuteRun() - Refactored run.go to use shared execution logic - New session.go for session persistence (~/.moat/claude/sessions/) - Added git to dependency registry for Claude Code requirements - Added UseOAuth config field for OAuth credential mounting
Plugin management (PR #35) followups: - Fix command injection risk in runMarketplacesUpdate() by using MarketplacePath() for consistent validation with defense-in-depth - Add cleanup helper functions (cleanupProxy, cleanupSSH, cleanupClaude) that log errors instead of discarding them - Fix claudeGenerated cleanup lifecycle by storing temp dir path in Run struct and cleaning up in Stop/Wait/Destroy - Add security tests for path traversal and URL parsing edge cases - Add package-level documentation (doc.go) Claude runner (PR #38) followups: - Document ~/.claude.json mount security model (writable by design) - Add comment about OAuth token prefix heuristic (sk-ant-oat) - Add debug logging for credential detection failures - Add debug logging for AnthropicSetup.Cleanup errors - Add debug logging for session state sync failures
- Remove dead store and misleading linter workaround in exec.go - Use strings.HasPrefix for clearer dependency check in hasDependency()
- Validate environment variable names match POSIX format - Resolve symlinks in workspace paths for consistent behavior - Add host validation in proxy credential methods (defense in depth) These changes improve error messages for user mistakes and add defense-in-depth validation for security-sensitive operations.
Since Moat runs Claude Code in an isolated container, the container itself serves as the security boundary. Permission prompts for file access and command execution are redundant when the workspace is already sandboxed. This adds --dangerously-skip-permissions to the claude command by default, providing a smoother experience. Users who want manual approval for each tool use can pass --noyolo to restore the default Claude behavior. The rationale is that Moat's isolation model (container + credential proxy) provides stronger security guarantees than per-operation prompts, while the prompts add friction without meaningful additional protection.
The provider-specific container mounts setup code was duplicated, causing credentials to be mounted twice to the same target path (e.g., /root/.claude/.credentials.json). This resulted in Docker rejecting the container creation with 'Duplicate mount point' error.
Claude Code refuses --dangerously-skip-permissions when running as root. This change creates a non-root user (moatuser, UID 1000) in generated container images so Claude Code can run with full permissions. Changes: - Add moatuser creation to generated Dockerfiles (UID 1000) - Install gosu for privilege dropping in entrypoint - Update moat-init.sh to drop privileges after SSH setup - Set USER directive for non-SSH containers - Set WORKDIR to /home/moatuser The moat-init entrypoint (used when SSH grants are present) runs as root to create the SSH socket, then uses gosu to drop to moatuser before executing the user's command. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
- SSH socket permissions: Changed from mode 0777 to 0660 with proper ownership (chown to moatuser:moatuser) for tighter access control - Marketplace names: Reject whitespace-only and control character names to prevent potential injection or filesystem issues - Proxy logging: Add debug logging when invalid hosts are rejected in SetCredentialHeader and AddExtraHeader for easier debugging 🤖 Generated with [Claude Code](https://claude.com/claude-code)
High priority fixes: - SSH socket validation: Verify socat process is running and socket was created before proceeding, with warning messages on failure - UID 1000 conflict: Delete existing user with UID 1000 before creating moatuser to avoid conflicts with base images Medium priority fixes: - hasDependency(): Add length check to ensure version exists after @ - ExtractHost(): Fix IPv6 literal parsing (e.g., git@[::1]:repo.git) - MarketplacePath(): Allow "foo..bar" as valid filename, only reject ".." as complete path traversal (since / and \ are already rejected) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
5d4b219 to
e42f57c
Compare
📦 Previous Review (superseded by newer review)Pull Request Review: Non-Root Container Execution and Security ImprovementsThis is a comprehensive PR that addresses multiple security concerns and improves the UX for running Claude Code in containers. The changes are well-structured and the commit history shows thoughtful iteration based on feedback. Code Quality: ExcellentStrengths:
Areas for consideration:
Potential Bugs: Minor Issues
Performance: Good
Security: StrongMajor improvements:
Concerns:
Test Coverage: ExcellentNew tests added:
Suggestions:
RecommendationsMust fix before merge:
Should consider:
Overall Assessment: ApproveThis is high-quality work that significantly improves both security and UX. The code demonstrates thoughtful security design, excellent test coverage, clear documentation, and proper error handling. The issues identified are minor and mostly edge cases. The PR is ready to merge with optional follow-up improvements. Impact Summary:
Great work addressing the feedback from PRs 35 and 38! |
Previously, there were two issues preventing .claude.json from being available in the container: 1. CopyHostClaudeFiles was only called when there was an OAuth token (needsClaudeInit=true). Users with API keys or just plugins configured would not get their onboarding state copied. 2. The moat-init.sh script copied files to $HOME, which is /root when running as root. After dropping privileges to moatuser, Claude Code looks in /home/moatuser/.claude.json - which didn't exist. Changes: - Extract host file copying into separate CopyHostClaudeFiles function - Call CopyHostClaudeFiles whenever a staging directory is created - Also create staging dir when running Claude Code (ShouldSyncClaudeLogs) - Fix moat-init.sh to copy files to /home/moatuser when running as root with moatuser available, instead of /root - Ensure proper ownership of copied files This ensures users don't have to re-complete onboarding in each container session.
Switch from complex file-based OAuth credential injection to using the CLAUDE_CODE_OAUTH_TOKEN environment variable. This is simpler and more reliable. Changes: - `moat grant anthropic` now runs `claude setup-token` to obtain a long-lived OAuth token (falls back to legacy methods if unavailable) - ContainerEnv now injects CLAUDE_CODE_OAUTH_TOKEN for OAuth tokens - Remove need for complex staging directory credential file setup - Update tests for new behavior The env var approach eliminates issues with file permissions, path remapping, and onboarding state not being preserved correctly.
Pull Request Review: Non-Root Container Execution and Security ImprovementsThis is a well-engineered PR that addresses important security concerns and improves UX. The implementation quality is high with excellent defense-in-depth security practices. However, there are a few critical issues to address. Overall AssessmentStrengths: Clean code, comprehensive path traversal protection, effective code deduplication, strong security test coverage Concerns: Unsafe fallback to root execution, missing test coverage for UID mapping, security implications of --dangerously-skip-permissions default Critical Issues1. Unsafe Fallback to Root Execution (internal/deps/scripts/moat-init.sh:103-112)If container starts as root but moatuser doesn't exist, script silently executes as root, defeating the security model. Fix: Fail with clear error instead of falling back to root. 2. UID 1000 Collision (internal/deps/dockerfile.go:47-50)Deleting existing UID 1000 user doesn't change file ownership. New moatuser inherits access to those files. Fix: Use higher UID (5000+), add validation, or document clearly. High Priority3. Missing UID Mapping TestsLinux UID mapping with UID 1000 special-casing lacks tests. Critical for security boundaries. 4. Token Extraction Fragility (cmd/moat/cli/grant.go:404-415)Hardcoded prefix without format validation. Add regex validation. 5. --dangerously-skip-permissions DefaultChanges user expectations. Consider: warning on first run, opt-in initially, audit logging, or clearer flag name. Positive HighlightsExcellent Command Injection Prevention (cmd/moat/cli/claude.go:24-47): Defense-in-depth with centralized validation and comprehensive tests. Clean Refactoring: Cleanup helpers eliminate duplication effectively. Strong Documentation (internal/claude/doc.go): Clear package docs improve contributor experience. Comprehensive Security Tests: Path traversal tests cover all edge cases. SummarySignificant security improvement. High code quality and good documentation. Required: Fix root execution fallback (#1) and consider UID collision (#2) Recommended: Add UID mapping tests, improve token validation, reconsider skip-permissions default Recommendation: Request changes for #1-2. Ready to merge once addressed. Great work! |
- Fail with clear error instead of silently running as root when moatuser doesn't exist in container (moat-init.sh) - Change container UID from 1000 to 5000 to avoid collision with existing users in base images (dockerfile.go, manager.go) - Add regex validation for OAuth token extraction to reject malformed tokens (grant.go) - Add comprehensive tests for UID mapping logic (manager_test.go) - Add tests for malformed token rejection (grant_test.go) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Code Review: Non-Root Container Execution & Security ImprovementsThis is a substantial and well-executed PR that addresses multiple security concerns, improves error handling, and enables --dangerously-skip-permissions by default through non-root container execution. Overall, the code quality is high and the changes are well-tested. ✅ StrengthsSecurity Improvements
Code Quality
🔍 Issues & SuggestionsHigh Priority1. Potential issue in UID mapping (manager.go:1035-1045) Suggestion: Use os.Stat(opts.Workspace) to get the actual workspace owner UID/GID instead of the process UID/GID. 2. SSH socket validation timing (moat-init.sh:35-40) 3. User deletion may fail silently (dockerfile.go:131-132) Medium Priority
Low Priority
🔒 Security AssessmentStrengths:
Concerns:
📝 RecommendationsBefore Merge
Post-Merge
✨ ConclusionHigh-quality PR that significantly improves security posture. The main concern is the UID mapping assumption which could cause permission issues. Once addressed, ready to merge. Recommendation: Approve with minor changes requested. Great work! 🚀 |
- Use workspace owner UID/GID instead of process UID for container user mapping. This fixes permission issues when moat is run with sudo or as a different user than the workspace owner. - Increase SSH socket creation timeout from 0.5s to 2s to handle slower socat startup - Improve user deletion error handling in Dockerfile generation - log warnings instead of silently suppressing all errors 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Security Review: Non-Root Container Execution and Security ImprovementsExecutive SummaryThis PR introduces significant security enhancements including non-root container execution, input validation improvements, path traversal prevention, and better error handling. The changes are well-implemented with good test coverage. I have identified a few areas for improvement, but no critical security vulnerabilities. Security IssuesMEDIUM Priority1. Potential Root Execution When Workspace is Root-OwnedLocation: Issue: The UID mapping logic uses the workspace owner UID, but does not prevent running as UID 0 if the workspace is owned by root. Recommendation: Add validation to prevent running as root when workspaceUID == 0 2. UID 5000 Collision Risk in Non-Standard EnvironmentsLocation: Issue: While UID 5000 is chosen to avoid conflicts with typical user ranges, some enterprise systems use UIDs starting at 5000. Recommendation: Consider using a higher UID range (e.g., 10000+) or document the UID 5000 choice in user-facing docs for enterprise environments. LOW Priority
Security Best Practices - Excellent Implementations1. Path Traversal PreventionLocation: Excellent defense-in-depth implementation with 13 comprehensive test cases covering edge cases! 2. Command Injection PreventionLocation: Well-designed fix with multiple validation layers using MarketplacePath() and filepath.Clean comparison. 3. Constant-Time AuthenticationLocation: Uses subtle.ConstantTimeCompare to prevent timing attacks - good security practice! 4. Privilege-Dropping Script SecurityLocation: Well-designed mechanism using gosu with proper validation and safe failure handling. Test Coverage AssessmentStrong Coverage:
Missing Coverage:
Recommendation: Add unit tests for env var validation and UID mapping edge cases. Overall AssessmentGrade: A- This is a well-executed security enhancement with strong attention to detail. The PR significantly improves security posture by:
Identified issues are minor and mostly represent opportunities for further hardening rather than actual vulnerabilities. Recommendations SummaryShould Address:
Nice to Have:
Recommendation: Approve with minor suggestions for follow-up improvements. The security enhancements are sound and the code quality is high. Great work on the comprehensive test coverage and security-first approach! |
This PR addresses code review feedback from PRs #35 and #38, adds several enhancements for better UX and
security, and implements non-root container execution to enable --dangerously-skip-permissions by default.
Non-Root Container Execution
Containers now run as moatuser (UID 1000) instead of root, enabling Claude Code's
--dangerously-skip-permissions flag which was previously blocked for security reasons when running as root.
permissions work correctly regardless of host user
--dangerously-skip-permissions by Default
Since Moat runs Claude Code in an isolated container, the container itself serves as the security boundary.
Permission prompts are now skipped by default, with --noyolo available to restore them.
Security Fixes
path functions
workspace paths, and host validation in proxy credential methods
Error Handling Improvements
silently discarding them
Documentation & Code Quality
Other