feat(agents): Conveniences for running Claude and support for OAuth based authentication#38
Conversation
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
PR Review: Claude Code IntegrationThis PR adds a comprehensive ✅ StrengthsArchitecture & Design
Code Quality
🔍 Issues Found1. Race Condition in Session State Updates (Medium Priority)Location: The session state synchronization has a potential race condition: for _, s := range sessions {
r, err := runMgr.Get(s.RunID)
// ... determine newState ...
if s.State != newState {
s.State = newState
_ = sessionMgr.UpdateState(s.ID, newState) // Error ignored
}
}Issues:
Recommendation: At minimum, log the error. Consider whether state updates should be atomic or if eventual consistency is acceptable here. 2. Unused Variable Assignment (Low Priority)Location: lastSigTime = now
_ = lastSigTime // Mark as used for linter (value read in next iteration)This is a workaround for a linter warning, but it's not ideal. The variable IS used in the next iteration. Recommendation: Remove the 3. Path Validation in Session Manager (Low Priority)Location: The
Current code: if !validSessionID.MatchString(session.ID) {
return fmt.Errorf("invalid session ID: %s", session.ID)
}
sessionDir := filepath.Join(m.dir, session.ID)Recommendation: Add an extra safety check: sessionDir := filepath.Join(m.dir, session.ID)
if !strings.HasPrefix(sessionDir, m.dir) {
return fmt.Errorf("invalid session ID: directory traversal detected")
}4. Potential OAuth Token Expiry HandlingLocation: The Recommendation: Consider adding expiry checking when loading credentials and warning users if their OAuth token is expired. 5. File Permissions on Credentials Directory (Security)Location: baseDir := filepath.Join(home, ".moat", "claude-creds")
if mkdirErr := os.MkdirAll(baseDir, 0700); mkdirErr != nil {Good use of 6. Missing Error ContextLocation: if sessionErr = sessionMgr.Create(absPath, r.ID, r.Name, grants); sessionErr != nil {
log.Debug("failed to create session", "error", sessionErr)
}Session creation failures are only logged at debug level. If sessions are an important feature, this should at least be a warning. Users might wonder why their sessions aren't showing up. 💡 Suggestions1. Code Duplication in TestsThe 2. Constants for Magic StringsLocation: return len(token) > 10 && token[:10] == "sk-ant-oat"Consider defining the OAuth token prefix as a constant for better maintainability: const OAuthTokenPrefix = "sk-ant-oat"
func IsOAuthToken(token string) bool {
return len(token) > len(OAuthTokenPrefix) &&
strings.HasPrefix(token, OAuthTokenPrefix)
}3. Time Format ConsistencyLocation: The 4. Documentation EnhancementThe README examples are excellent, but consider adding:
5. Dependency DeclarationLocation: The 🎯 Performance Considerations
🔒 Security ReviewPositive
Concerns
📊 Test Coverage AssessmentExcellent test coverage overall:
🎨 Code Style & ConventionsThe code follows the project's conventions well:
Minor: The comment style is mostly consistent, but some places use 📝 DocumentationThe documentation updates in README.md are clear and helpful. The examples tell a good story. One suggestion: add a troubleshooting section for common issues like:
Final VerdictThis is a high-quality PR with good architecture, solid test coverage, and thoughtful implementation. The issues found are mostly minor with a few medium-priority items around error handling and edge cases. Recommended Actions Before Merge:
The PR is ready for merge after addressing the high-priority items. Great work! 🚀 |
Code Review: OAuth and Claude Code SupportThis is high-quality work with excellent abstractions. Here are my findings: Strengths:
Critical Issues:
Medium Priority:
Minor:
Security: Good overall. Credentials properly injected, secure file permissions, directory traversal prevention. Performance: Session listing scales linearly - fine for typical usage. Overall: Approve with minor fixes recommended. Core functionality is solid. Main action items: fix shadowed variable bug and ensure cleanup reliability. |
Code Review - PR #38: Claude Code IntegrationThis is an excellent addition to Moat! The implementation is well-structured, thoroughly tested, and follows the project's conventions. Here's my detailed review: 🎯 Overall AssessmentStrong Points:
📝 Code QualityArchitecture (Excellent)The refactoring to extract shared execution logic into
Security (Good with minor note)Credential Handling:
Note: The OAuth token detection in func IsOAuthToken(token string) bool {
return len(token) > 10 && token[:10] == "sk-ant-oat"
}This is a reasonable heuristic but might be fragile if Anthropic changes token prefixes. Consider adding a comment about this being based on observed token format. File Mounts (Potential Issue)In mounts = append(mounts, container.MountConfig{
Source: hostClaudeJSON,
Target: filepath.Join(containerHome, ".claude.json"),
ReadOnly: false, // ⚠️
})Question: Is this intentional? Allowing the container to modify the host's
Session Management (Good)Strengths:
Minor improvement: The session state sync in Error Messages (Excellent)Following the CLAUDE.md guidance perfectly. Examples: "Claude Code credentials not found at %s\n" +
" Have you logged into Claude Code? Run 'claude' to authenticate first"Clear, actionable, tells users exactly what to do. 🐛 Potential Issues1. Path Injection in Session ID (Low Risk)
if !validSessionID.MatchString(id) {
return fmt.Errorf("invalid session ID: %s", id)
}
sessionDir := filepath.Join(m.dir, id)Good validation present! The regex prevents path traversal. ✅ 2. Race Condition in Cleanup (Low Risk)
func (a *AnthropicSetup) Cleanup(cleanupPath string) {
if cleanupPath != "" {
_ = os.RemoveAll(cleanupPath)
}
}If cleanup fails, it's silently ignored. This is probably fine for temp directories, but consider logging at debug level. 3. Missing Validation (Low Risk)
if hasAnthropicCredential() {
addGrant("anthropic")
}
🚀 Performance ConsiderationsMount OptimizationThe code conditionally mounts host files only if they exist (good!): if _, err := os.Stat(hostClaudeJSON); err == nil {
mounts = append(mounts, ...)
}This avoids unnecessary mounts. ✅ Session Listing
Not urgent - unlikely to have enough sessions for this to matter in practice. ✅ Test Coverage (Excellent)Well-structured tests covering:
📚 DocumentationREADME Updates (Good)
Minor suggestion:The README shows: Found Claude Code credentials.
Subscription: claude_pro
Expires: 2025-02-15T10:30:00ZConsider using a relative time format ("expires in 25 days") for better UX, matching the 🔧 RecommendationsHigh Priority1. Review
Medium Priority2. Add debug logging for credential detection func hasAnthropicCredential() bool {
key, err := credential.DefaultEncryptionKey()
if err != nil {
log.Debug("failed to get encryption key", "error", err) // Add this
return false
}
// ... rest of function
}3. Add cleanup error logging func (a *AnthropicSetup) Cleanup(cleanupPath string) {
if cleanupPath != "" {
if err := os.RemoveAll(cleanupPath); err != nil {
log.Debug("failed to cleanup credentials dir", "path", cleanupPath, "error", err)
}
}
}Low Priority (Nice to Have)4. Add integration test for the full 5. Consider adding session cleanup command: 6. Token refresh: The OAuth token handling doesn't include refresh logic. Document whether refresh is handled by Claude Code itself or if tokens are long-lived. ✨ Excellent Patterns to Highlight
🎬 ConclusionThis is production-ready code with just a few minor improvements to consider. The architecture is solid, tests are comprehensive, and it follows all the project conventions. Great work! Approve with minor suggestions - the high-priority item about Review generated with analysis of 22 files, 2655 additions, and focused testing of credential/session management |
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
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
… as default (#42) * feat(claude): Add `moat claude` command for easy Claude Code execution 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 * feat(agents): Conveniences for running Claude. Support non-API usage. * fix(claude): Address code review feedback for plugin and runner PRs 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 * style(cli): Minor code polish - Remove dead store and misleading linter workaround in exec.go - Use strings.HasPrefix for clearer dependency check in hasDependency() * fix(cli): Add input validation for better error handling - 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. * feat(claude): Enable --dangerously-skip-permissions by default 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. * fix(run): Remove duplicate provider mounts code block 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. * feat(deps): Run containers as non-root moatuser 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) * style: Fix gofmt formatting in runtime.go * fix(security): Address code review feedback - 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) * fix: Address code review feedback for PR #42 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) * fix(claude): Always copy host .claude.json to container 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. * feat(claude): Use CLAUDE_CODE_OAUTH_TOKEN for authentication 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. * fix(security): Address code review feedback - 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) * fix: Address additional code review feedback - 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)
Details
New moat claude command:
Credential support:
Session tracking:
Code improvements: