Skip to content

Conversation

@webdevcody
Copy link
Collaborator

@webdevcody webdevcody commented Dec 21, 2025

Summary by CodeRabbit

  • Configuration Changes

    • Replaced project-dir restriction with a single ALLOWED_ROOT_DIRECTORY; removed legacy Google/OpenAI API key fields
  • Security Improvements

    • Enforced path validation for file and project operations; added explicit forbidden (403) handling for disallowed paths
    • Switched file I/O to a validated/secure filesystem layer
  • Infrastructure

    • Added GitHub CLI to the production container
  • UI / UX

    • Improved workspace/default-folder handling, persistence of last project directory, and directory-selection feedback

✏️ Tip: You can customize this high-level summary in your review settings.

Test User and others added 7 commits December 20, 2025 15:59
…dation

This commit consolidates directory security from two environment variables
(WORKSPACE_DIR, ALLOWED_PROJECT_DIRS) into a single ALLOWED_ROOT_DIRECTORY variable
while maintaining backward compatibility.

Changes:
- Re-enabled path validation in security.ts (was previously disabled)
- Implemented isPathAllowed() to check ALLOWED_ROOT_DIRECTORY with DATA_DIR exception
- Added backward compatibility for legacy ALLOWED_PROJECT_DIRS and WORKSPACE_DIR
- Implemented path traversal protection via isPathWithinDirectory() helper
- Added PathNotAllowedError custom exception for security violations
- Updated all FS route endpoints to validate paths and return 403 on violation
- Updated template clone endpoint to validate project paths
- Updated workspace config endpoints to use ALLOWED_ROOT_DIRECTORY
- Fixed stat() response property access bug in project-init.ts
- Updated security tests to expect actual validation behavior

Security improvements:
- Path validation now enforced at all layers (routes, project init, agent services)
- appData directory (DATA_DIR) always allowed for settings/credentials
- Backward compatible with existing ALLOWED_PROJECT_DIRS/WORKSPACE_DIR configurations
- Protection against path traversal attacks

Backend test results: 654/654 passing ✅

🤖 Generated with Claude Code

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Removed all references to WORKSPACE_DIR environment variable to simplify
configuration. The system now uses exclusively ALLOWED_ROOT_DIRECTORY
for controlling the root directory where projects can be accessed.

Changes:
- Removed WORKSPACE_DIR from security.ts initialization
- Updated workspace/routes/directories.ts to require ALLOWED_ROOT_DIRECTORY
- Updated workspace/routes/config.ts to require ALLOWED_ROOT_DIRECTORY
- Updated apps/ui/src/main.ts to use ALLOWED_ROOT_DIRECTORY instead of WORKSPACE_DIR
- Updated .env file to reference ALLOWED_ROOT_DIRECTORY
- Removed WORKSPACE_DIR test from security.test.ts

Backend test results: 653/653 passing ✅

🤖 Generated with Claude Code

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Removed all references to OPENAI_API_KEY and GOOGLE_API_KEY since only
Claude (Anthropic) provider is implemented. These were placeholder references
for future providers that don't exist yet.

Changes:
- Removed OPENAI_API_KEY and GOOGLE_API_KEY from docker-compose.yml
- Removed from .env and .env.example files
- Updated setup/routes/store-api-key.ts to only support anthropic
- Updated setup/routes/delete-api-key.ts to only support anthropic
- Updated setup/routes/api-keys.ts to only return anthropic key status
- Updated models/routes/providers.ts to only list anthropic provider
- Updated auto-mode-service.ts error message to only reference ANTHROPIC_API_KEY

Backend test results: 653/653 passing ✅

🤖 Generated with Claude Code

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This fixes a critical security issue where path parameters from client requests
were not validated against ALLOWED_ROOT_DIRECTORY, allowing attackers to access
files and directories outside the configured root directory.

Changes:
- Add validatePath() checks to 29 route handlers that accept path parameters
- Validate paths in agent routes (workingDirectory, imagePaths)
- Validate paths in feature routes (projectPath)
- Validate paths in worktree routes (projectPath, worktreePath)
- Validate paths in git routes (projectPath, filePath)
- Validate paths in auto-mode routes (projectPath, worktreePath)
- Validate paths in settings/suggestions routes (projectPath)
- Return 403 Forbidden for paths outside ALLOWED_ROOT_DIRECTORY
- Maintain backward compatibility (unrestricted when env var not set)

Security Impact:
- Prevents directory traversal attacks
- Prevents unauthorized file access
- Prevents arbitrary code execution via unvalidated paths

All validation follows the existing pattern in fs routes and session creation,
using the validatePath() function from lib/security.ts which checks against
both ALLOWED_ROOT_DIRECTORY and DATA_DIR (appData).

Tests: All 653 unit tests passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit replaces direct file system operations with a secure file system adapter to enhance security by enforcing path validation. The changes include:

- Replaced `fs` imports with `secureFs` in various services and utilities.
- Updated file operations in `agent-service`, `auto-mode-service`, `feature-loader`, and `settings-service` to use the secure file system methods.
- Ensured that all file I/O operations are validated against the ALLOWED_ROOT_DIRECTORY.

This refactor aims to prevent unauthorized file access and improve overall security posture.

Tests: All unit tests passing.

🤖 Generated with Claude Code
…y support

This commit refactors the handling of ALLOWED_ROOT_DIRECTORY by removing legacy support for ALLOWED_PROJECT_DIRS and simplifying the security logic. Key changes include:

- Removed deprecated ALLOWED_PROJECT_DIRS references from .env.example and security.ts.
- Updated initAllowedPaths() to focus solely on ALLOWED_ROOT_DIRECTORY and DATA_DIR.
- Enhanced logging for ALLOWED_ROOT_DIRECTORY configuration status.
- Adjusted route handlers to utilize the new workspace directory logic.
- Introduced a centralized storage module for localStorage operations to improve consistency and error handling.

These changes aim to enhance security and maintainability by consolidating directory management into a single variable.

Tests: All unit tests passing.
@coderabbitai
Copy link

coderabbitai bot commented Dec 21, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Introduces ALLOWED_ROOT_DIRECTORY-based path enforcement, a secure-fs adapter that validates paths, and middleware to validate request path parameters; replaces fs/promises usage with secure-fs across server services and routes; removes Google/OpenAI API key support; adds UI storage and workspace-config helpers and updates related components and tests.

Changes

Cohort / File(s) Summary
Security core & secure FS
apps/server/src/lib/security.ts, apps/server/src/lib/secure-fs.ts
Adds ALLOWED_ROOT_DIRECTORY enforcement, PathNotAllowedError, init/getters, validatePath/isPathAllowed, and a secure filesystem adapter that validates paths before delegating to Node fs.
Middleware
apps/server/src/middleware/validate-paths.ts
New Express middleware factory validatePathParams(...) to validate path params in request bodies using validatePath.
Server: fs → secure-fs migrations
apps/server/src/lib/fs-utils.ts, apps/server/src/lib/automaker-paths.ts, apps/server/src/lib/image-handler.ts, apps/server/src/services/*, apps/server/src/routes/*
Replaces direct fs/promises calls with secure-fs wrappers and inserts isPathAllowed/validatePath checks across services and many route handlers; removes addAllowedPath side-effects.
Routes: middleware wiring
apps/server/src/routes/agent/index.ts, .../auto-mode/index.ts, .../features/index.ts, .../git/index.ts, .../settings/index.ts, .../suggestions/index.ts, .../worktree/index.ts
Applies validatePathParams to many routes to enforce parameter validation before handlers.
Routes: explicit path error handling
apps/server/src/routes/fs/routes/*.ts, apps/server/src/routes/templates/routes/clone.ts, apps/server/src/routes/fs/routes/validate-path.ts, apps/server/src/routes/fs/routes/resolve-directory.ts
Uses isPathAllowed/PathNotAllowedError to return 403 for disallowed paths; removes addAllowedPath mutations.
Config & container updates
apps/server/.env.example, docker-compose.yml, docker-compose.override.yml.example, apps/server/Dockerfile
Replaces ALLOWED_PROJECT_DIRS/WORKSPACE_DIR with ALLOWED_ROOT_DIRECTORY, removes Google/OpenAI env keys, and adds GH CLI install step in Dockerfile.
API keys / provider removal
apps/server/src/routes/models/routes/providers.ts, apps/server/src/routes/setup/routes/*, apps/server/src/types/settings.ts, apps/server/src/services/settings-service.ts
Removes Google/OpenAI API key handling from types, responses, and setup routes; keeps only Anthropic provider.
Workspace & UI storage
apps/ui/src/lib/storage.ts, apps/ui/src/lib/workspace-config.ts, apps/ui/src/lib/http-api-client.ts
Adds a storage wrapper (get/set/remove/getJSON/setJSON) and workspace-config helpers (getDefaultWorkspaceDirectory, saveLastProjectDirectory); extends workspace.getConfig return type with optional defaultDir.
UI components updated
apps/ui/src/components/dialogs/file-browser-dialog.tsx, .../new-project-modal.tsx, .../interview-view.tsx, .../welcome-view.tsx, apps/ui/src/lib/electron.ts
Replace direct localStorage with storage helpers, integrate workspace-config usage, validate directories before operations, and persist last project dir.
UI minor tweaks & tests
apps/ui/src/components/views/board-view/*, apps/ui/tests/*, apps/ui/src/hooks/use-settings-migration.ts, apps/ui/src/lib/project-init.ts
Tooltip/aria/test-id improvements, storage migration to abstraction, test selector/wait robustness, and project-init directory existence checks.
Tests & test setup
apps/server/tests/setup.ts, apps/server/tests/unit/lib/security.test.ts, apps/server/tests/unit/services/settings-service.test.ts
Remove ALLOWED_PROJECT_DIRS setup; update security tests for ALLOWED_ROOT_DIRECTORY and settings tests to expect only Anthropic key.
Misc / formatting
apps/server/src/routes/agent/routes/send.ts, apps/server/src/routes/agent/routes/start.ts, docker-compose.yml, docker-compose.override.yml.example
Minor whitespace/comment updates and small Docker compose comment adjustments.

Sequence Diagram(s)

sequenceDiagram
    participant Client as HTTP Client
    participant Middleware as validatePathParams
    participant Route as Route Handler
    participant SecureFS as secure-fs Adapter
    participant Security as security module
    participant NodeFS as Node fs

    Client->>Middleware: POST /api/... with path param(s)
    Middleware->>Security: validatePath(param)
    Security->>Security: resolve & check ALLOWED_ROOT_DIRECTORY / DATA_DIR
    alt allowed
        Security-->>Middleware: resolved path
        Middleware->>Route: next()
        Route->>SecureFS: perform I/O (readFile/mkdir/etc.) with resolved path
        SecureFS->>Security: validatePath(path)  /* double-check */
        Security-->>SecureFS: ok
        SecureFS->>NodeFS: fs operation
        NodeFS-->>SecureFS: result
        SecureFS-->>Route: data / success
        Route-->>Client: 200 OK
    else not allowed
        Security-->>Middleware: throw PathNotAllowedError
        Middleware-->>Client: 403 Forbidden + message
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • apps/server/src/lib/security.ts — path resolution, isPathWithinDirectory, symlink/traversal edge cases.
  • apps/server/src/lib/secure-fs.ts — ensure every wrapper validates correctly and preserves error semantics.
  • Critical services using secure-fs: apps/server/src/services/auto-mode-service.ts, agent-service.ts, feature-loader.ts — consistency of resolved paths and error flows.
  • Routes where addAllowedPath was removed (templates, fs, features) — confirm intended behavior and no regressions.
  • Settings/types changes: apps/server/src/types/settings.ts and migration logic in settings-service.ts — verify clients and tests align with key shape change.

Possibly related PRs

Suggested labels

Refactor

Poem

🐰 I hopped through code with careful paws,

Replaced loose paths with guarded laws.
SecureFs checks each burrowed way,
Anthropic stays, Google hops away.
A tidy warren, safe and bright—hooray!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 59.74% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The PR title 'Improve code docker2' is vague and generic, using non-descriptive terms that fail to convey the actual scope of changes, which include path-based security improvements, filesystem wrapper implementation, API provider consolidation, and extensive route middleware additions. Consider a more descriptive title that captures the primary changes, such as 'Implement path-based security validation and consolidate to Anthropic provider' or 'Add secure filesystem wrapper with path validation middleware'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch improve-code-docker2

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @webdevcody, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the application's security posture by implementing a robust, centralized file system access control mechanism. It introduces a new ALLOWED_ROOT_DIRECTORY configuration, ensuring all file operations are strictly confined to designated areas, thereby mitigating potential path traversal vulnerabilities. This change involved extensive refactoring of file system interactions across the server, the addition of a dedicated security middleware, and updates to the UI's workspace management to align with these new restrictions. Additionally, support for Google API keys has been removed, and the Docker build process has been improved with essential tools.

Highlights

  • Enhanced File System Security: Introduced a new secure-fs module and ALLOWED_ROOT_DIRECTORY environment variable to enforce strict path validation for all file system operations, preventing path traversal attacks. All file I/O operations across the server now route through this secure module.
  • Centralized Path Validation Middleware: A new Express middleware (validate-paths.ts) was added to automatically validate path parameters in incoming API requests, ensuring security at the API layer and returning a 403 Forbidden error if validation fails.
  • Removed Google API Key Support: The server-side and UI code related to Google API keys (previously for Gemini support) has been removed, simplifying the codebase and reducing external dependencies.
  • Docker Image Improvements: The Dockerfile now includes the installation of git, curl, and GitHub CLI in the production stage, making the Docker environment more self-sufficient for development and CI/CD.
  • UI Workspace Configuration Refinement: The UI now leverages a new workspace-config.ts utility to determine default project directories, improving consistency and integrating with the new ALLOWED_ROOT_DIRECTORY server-side setting.
  • LocalStorage Abstraction: A new storage.ts utility has been introduced in the UI to centralize and abstract localStorage operations, improving maintainability and error handling.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@claude
Copy link

claude bot commented Dec 21, 2025

Code Review - PR #201: Improve Docker & Security

Overall Assessment

This is a significant security-focused refactor that introduces comprehensive path validation and file system access controls. The implementation demonstrates a defense-in-depth approach with validation at multiple layers. However, there are several important issues that need to be addressed before merging.

Status: ⚠️ Changes Requested


🔴 Critical Issues

1. Type Safety Violations in secure-fs.ts

File: apps/server/src/lib/secure-fs.ts

The return types for several functions use any, which defeats TypeScript's type safety:

// Lines 72, 137
export async function stat(filePath: string): Promise<any>
export async function lstat(filePath: string): Promise<any>

Issue: Using any removes compile-time type checking and defeats the purpose of TypeScript. This violates the clean code guidelines (Section 11: Type Safety).

Recommendation: Import and use the proper Stats type from fs:

import fs, { type Stats } from "fs/promises";

export async function stat(filePath: string): Promise<Stats> {
  const validatedPath = validatePath(filePath);
  return fs.stat(validatedPath);
}

export async function lstat(filePath: string): Promise<Stats> {
  const validatedPath = validatePath(filePath);
  return fs.lstat(validatedPath);
}

2. Inconsistent Type Casting Pattern

Files: Multiple service files

Throughout the codebase, there's inconsistent type casting when using secureFs.readFile:

// Sometimes with 'as string' cast
const context = await secureFs.readFile(contextPath, "utf-8") as string;

// Sometimes with 'as Buffer' cast  
const imageBuffer = await secureFs.readFile(imagePath) as Buffer;

// Sometimes returned as string[] | any[]
return fs.readdir(validatedPath, options as any);

Issue: The overloaded signature in readFile returns string | Buffer, but the implementation doesn't properly type-narrow based on the encoding parameter. This forces consumers to use type assertions everywhere, which is error-prone.

Recommendation: Use function overloads to provide proper type narrowing:

export async function readFile(filePath: string): Promise<Buffer>;
export async function readFile(filePath: string, encoding: BufferEncoding): Promise<string>;
export async function readFile(
  filePath: string,
  encoding?: BufferEncoding
): Promise<string | Buffer> {
  const validatedPath = validatePath(filePath);
  if (encoding) {
    return fs.readFile(validatedPath, encoding);
  }
  return fs.readFile(validatedPath);
}

This would eliminate the need for type assertions at call sites.


🟡 Medium Priority Issues

3. Missing Path Validation in Route Handlers

Files: Various route files

While the middleware validatePathParams is excellent, it only validates paths that exist in req.body. Some routes may accept paths via query parameters or URL params, which aren't covered.

Example locations to check:

  • Query parameters in GET requests
  • Path parameters in route definitions (e.g., /api/file/:path)

Recommendation: Audit all routes to ensure path validation covers all input vectors, not just req.body.

4. Path Validation Error Handling Inconsistency

Files: apps/server/src/routes/fs/routes/*.ts

Some route handlers manually catch PathNotAllowedError and return 403:

} catch (error) {
  if (error instanceof PathNotAllowedError) {
    res.status(403).json({ success: false, error: getErrorMessage(error) });
    return;
  }
  // ...
}

But the middleware validatePathParams already handles this. This creates redundant error handling and potential double-responses.

Recommendation:

  • If using the validatePathParams middleware, remove manual catching of PathNotAllowedError in route handlers
  • OR, ensure the middleware is consistently applied to all routes that need validation
  • Document which approach should be used where

5. Unsafe Type Assertion in Middleware

File: apps/server/src/middleware/validate-paths.ts:44

if (Array.isArray(values) && values.length > 0) {
  for (const value of values) {
    validatePath(value);
  }
}

Issue: No type checking on array elements before calling validatePath(value). If the array contains non-string values, this could cause runtime errors.

Recommendation:

if (Array.isArray(values) && values.length > 0) {
  for (const value of values) {
    if (typeof value === 'string') {
      validatePath(value);
    } else {
      throw new Error(`Invalid path parameter: expected string, got ${typeof value}`);
    }
  }
}

6. Docker Security: Running as Root

File: apps/server/Dockerfile:35-49

The Dockerfile creates a non-root user but may not be using it for all operations. Installing packages as root is appropriate, but ensure the final USER directive is set.

Recommendation: Verify that the final container runs as the automaker user, not root. Add explicit USER automaker before the CMD/ENTRYPOINT.


🟢 Positive Observations

Excellent Security Architecture ✅

The layered security approach is well-designed:

  1. Route middleware - Early validation at API boundary
  2. Service layer - Additional checks in business logic
  3. File system adapter - Enforcement at I/O level

This defense-in-depth strategy is a best practice.

Good Abstraction Pattern ✅

The secure-fs.ts module is a clean abstraction that:

  • Centralizes security logic
  • Makes security enforcement transparent
  • Follows the Single Responsibility Principle
  • Easy to audit and maintain

Comprehensive Test Coverage ✅

The security tests in apps/server/tests/unit/lib/security.test.ts cover:

  • Edge cases (path traversal attempts)
  • Configuration variations
  • Both positive and negative scenarios

Great job here!

Clean Middleware Design ✅

The validatePathParams middleware with special syntax (? for optional, [] for arrays) is elegant and reusable.

Proper Error Types ✅

Creating a custom PathNotAllowedError class is the right approach for distinguishing security violations from other errors.


🔵 Code Quality & Best Practices

Follows Clean Code Guidelines ✅

Based on docs/clean-code.md:

  • ✅ DRY principle: Security logic centralized
  • ✅ Reusability: secure-fs module is highly reusable
  • ✅ Good abstractions: Clear separation of concerns
  • ✅ Meaningful names: Functions clearly describe intent
  • ⚠️ Type safety: Needs improvement (see Critical Issue feat(sidebar): implement trash functionality for project management #1)
  • ✅ Single Responsibility: Each module has clear purpose

Documentation Quality ✅

Functions have clear JSDoc comments explaining:

  • Purpose
  • Parameters
  • Return values
  • Edge cases

Consistency ⚠️

Minor inconsistency: Some files import as import * as secureFs while others could benefit from named imports. Consider standardizing.


🛡️ Security Analysis

Strengths

  1. Path Traversal Protection: The isPathWithinDirectory function correctly uses path.relative() and checks for .. prefixes
  2. Backward Compatibility: Graceful degradation when ALLOWED_ROOT_DIRECTORY is not set
  3. Data Directory Exception: Properly allows access to app data for settings/credentials
  4. Multiple Validation Layers: Defense in depth with middleware, service, and I/O validation

Potential Concerns

  1. Symlink Handling: The code uses lstat() in fs-utils.ts to detect symlinks, but secure-fs.ts doesn't explicitly handle symlinks. Consider if symlinks could bypass the ALLOWED_ROOT_DIRECTORY restriction by pointing outside the allowed directory.

    Recommendation: Document the expected behavior for symlinks, or add explicit symlink resolution and validation.

  2. Windows Path Handling: Path traversal checks using path.relative() should work cross-platform, but Windows has additional complexities (UNC paths, drive letters, case sensitivity). Consider adding explicit tests for Windows path edge cases.

  3. Race Conditions: Between path validation and actual file operation, the file system state could change. While unlikely to be exploitable here, it's worth noting for future security reviews.


📊 Performance Considerations

Minimal Impact ✅

The added validation overhead is negligible:

  • path.resolve() and path.relative() are fast operations
  • Validation happens once per request
  • No blocking I/O in validation path

Caching Opportunity 💡

If performance becomes a concern under high load, consider caching validation results for frequently-accessed paths. However, this is likely premature optimization at this stage.


🧪 Test Coverage

Current Coverage ✅

  • Unit tests for security.ts covering core logic
  • Tests verify path traversal protection
  • Tests cover configuration variations
  • Backend tests: 653/653 passing ✅

Suggested Additional Tests

  1. Integration tests for the validatePathParams middleware
  2. Edge case tests for secure-fs.ts functions
  3. Windows-specific path tests (if Windows support is needed)
  4. Symlink behavior tests
  5. Concurrent access tests to validate thread safety

📝 Additional Observations

Environment Variable Changes

Good cleanup removing unused OPENAI_API_KEY and GOOGLE_API_KEY. This reduces configuration complexity and potential confusion.

UI Changes (storage.ts)

The new localStorage abstraction is clean and follows best practices:

  • Error handling with try-catch
  • Environment detection
  • Type-safe JSON helpers
  • Single Responsibility Principle

Middleware Pattern

The special syntax in validatePathParams (paramName?, paramName[]) is creative but could benefit from additional documentation in the function's JSDoc.


🎯 Recommendations Summary

Before Merging (Critical):

  1. Fix type safety issues in secure-fs.ts (use Stats type instead of any)
  2. Improve type narrowing in readFile overloads to eliminate type assertions
  3. Add type checking for array elements in middleware
  4. Verify Docker container runs as non-root user

Follow-up Tasks (Medium Priority):

  1. Audit query and URL parameter validation
  2. Resolve error handling redundancy between middleware and route handlers
  3. Add explicit symlink handling documentation/tests
  4. Add Windows-specific path tests
  5. Document the middleware special syntax more clearly

Future Enhancements (Low Priority):

  1. Consider performance monitoring under high load
  2. Add integration tests for middleware
  3. Consider explicit UNC path and drive letter handling for Windows

✅ Approval Recommendation

This PR represents a major security improvement and the overall architecture is sound. However, the type safety issues must be addressed before merging to maintain code quality standards.

Once the critical issues are fixed, this will be an excellent contribution to the codebase.

Great work on the security architecture! The defense-in-depth approach and comprehensive testing demonstrate thoughtful engineering.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces significant and well-executed security improvements by sandboxing file system access. The introduction of a secure file system adapter (secure-fs.ts), path validation middleware, and a unified ALLOWED_ROOT_DIRECTORY configuration provides strong defense-in-depth. The refactoring extends to the UI, with better abstractions for storage and workspace management. The Docker configuration is also updated to reflect these changes. Overall, this is an excellent enhancement to the application's security posture and robustness. I have a few suggestions to further improve the code, mainly around typing and removing some leftover code from a previous implementation.

encoding?: BufferEncoding
): Promise<void> {
const validatedPath = validatePath(filePath);
return fs.writeFile(validatedPath, data, encoding as any);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The use of as any here bypasses TypeScript's type safety. The options parameter of fs.writeFile is optional, so you can handle the encoding parameter in a type-safe way by conditionally providing it.

Suggested change
return fs.writeFile(validatedPath, data, encoding as any);
return fs.writeFile(validatedPath, data, encoding);

options?: { withFileTypes?: boolean; encoding?: BufferEncoding }
): Promise<string[] | any[]> {
const validatedPath = validatePath(dirPath);
return fs.readdir(validatedPath, options as any);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using as any for the options in fs.readdir is not type-safe and hides potential issues with the options object structure. The overloads for fs.readdir are specific about the combination of withFileTypes and encoding.

To maintain type safety, you can use function overloads in your wrapper to correctly mirror the behavior of fs.readdir.

Suggested change
return fs.readdir(validatedPath, options as any);
return fs.readdir(validatedPath, options);

encoding?: BufferEncoding
): Promise<void> {
const validatedPath = validatePath(filePath);
return fs.appendFile(validatedPath, data, encoding as any);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to writeFile, the as any cast for encoding in fs.appendFile is not ideal. The options argument is optional, so you can pass encoding directly to maintain type safety.

Suggested change
return fs.appendFile(validatedPath, data, encoding as any);
return fs.appendFile(validatedPath, data, encoding);

Comment on lines 62 to 64
export function addAllowedPath(filePath: string): void {
allowedPaths.add(path.resolve(filePath));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The addAllowedPath function and the allowedPaths set appear to be remnants of a previous implementation. The main security check in isPathAllowed relies solely on allowedRootDirectory and dataDirectory, and does not check the allowedPaths set.

This means that any calls to addAllowedPath throughout the codebase have no effect on the security validation, which could lead to incorrect assumptions about how the security model works. This dead code should be removed to avoid confusion.

Comment on lines 31 to 35
GH_VERSION=$(curl -s https://api.github.com/repos/cli/cli/releases/latest | grep '"tag_name"' | cut -d '"' -f 4 | sed 's/v//') && \
curl -L "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" -o gh.tar.gz && \
tar -xzf gh.tar.gz && \
mv gh_*_linux_amd64/bin/gh /usr/local/bin/gh && \
rm -rf gh.tar.gz gh_*_linux_amd64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For more deterministic and reliable builds, it's a good practice to pin the version of tools you install. Fetching the latest version of the GitHub CLI can introduce unexpected changes or breakages in your CI/CD pipeline.

Additionally, using a wildcard in the mv command (mv gh_*_linux_amd64/bin/gh) can be fragile if the extracted directory name format changes in a future release. Using the version variable would make this more robust.

I suggest pinning the version and adjusting the script accordingly.

    GH_VERSION="2.49.2" && \
    curl -L "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" -o gh.tar.gz && \
    tar -xzf gh.tar.gz && \
    mv "gh_${GH_VERSION}_linux_amd64/bin/gh" /usr/local/bin/gh && \
    rm -rf gh.tar.gz

await secureFs.access(contextDir);

const files = await fs.readdir(contextDir);
const files = await secureFs.readdir(contextDir) as string[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The cast to string[] here is incorrect when withFileTypes is true, as fs.readdir would return Dirent[]. This is a symptom of the as any cast in the secure-fs.ts wrapper. Once the readdir wrapper in secure-fs.ts is correctly typed with overloads, this cast can be corrected or removed.

Suggested change
const files = await secureFs.readdir(contextDir) as string[];
const files = (await secureFs.readdir(contextDir, { withFileTypes: true })) as fs.Dirent[];


try {
const entries = await fs.readdir(featuresDir, { withFileTypes: true });
const entries = await secureFs.readdir(featuresDir, { withFileTypes: true }) as any[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The cast to any[] here is hiding a type issue. When withFileTypes: true is used, fs.readdir returns an array of Dirent objects. This is related to the as any cast in the secure-fs wrapper. For type safety, this should be cast to fs.Dirent[] after the underlying issue in secure-fs is addressed.

Suggested change
const entries = await secureFs.readdir(featuresDir, { withFileTypes: true }) as any[];
const entries = (await secureFs.readdir(featuresDir, { withFileTypes: true })) as fs.Dirent[];


// Read all feature directories
const entries = await fs.readdir(featuresDir, { withFileTypes: true });
const entries = await secureFs.readdir(featuresDir, { withFileTypes: true }) as any[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The cast to any[] here is hiding a type issue. When withFileTypes: true is used, fs.readdir returns an array of Dirent objects. This is related to the as any cast in the secure-fs wrapper. For type safety, this should be cast to fs.Dirent[] after the underlying issue in secure-fs is addressed.

Suggested change
const entries = await secureFs.readdir(featuresDir, { withFileTypes: true }) as any[];
const entries = (await secureFs.readdir(featuresDir, { withFileTypes: true })) as fs.Dirent[];

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/server/src/services/auto-mode-service.ts (3)

943-1038: Add path validation to verifyFeature and commitFeature before running shell commands

Both methods execute shell commands using cwd derived from untrusted path parameters without validating the resolved path against the allowed directory whitelist. This creates a security vulnerability if projectPath, featureId, or providedWorktreePath could direct execution outside ALLOWED_ROOT_DIRECTORY/DATA_DIR.

Add validation at method entry and before each execAsync call:

  • Validate projectPath at the top of both methods
  • Validate resolved workDir before any execAsync call (after determining which path to use)

The necessary imports (isPathAllowed, PathNotAllowedError) are already present in the file and follow the pattern used in executeFeature (lines 491, 559).


1192-1234: Add path validation to analyzeProject for consistency and security

analyzeProject passes projectPath as cwd to an agent with file operation tools (Read, Glob, Grep) and saves analysis results via secureFs. While the write operations are protected, the agent tools operate relative to cwd, which should be validated just like in executeFeature. Add this check at the start of the method:

  async analyzeProject(projectPath: string): Promise<void> {
+   if (!isPathAllowed(projectPath)) {
+     throw new PathNotAllowedError(projectPath);
+   }
    const abortController = new AbortController();

727-891: followUpFeature lacks projectPath validation present in executeFeature

followUpFeature does not validate projectPath with isPathAllowed at the method entry, unlike executeFeature. This creates an inconsistent validation pattern where:

  • executeFeature validates projectPath early (line checks with isPathAllowed)
  • followUpFeature derives workDir from projectPath without validation
  • Both pass workDir to runAgent which executes relative to that directory
  • Image copying via secureFs.copyFile is protected (validates destination), but the working directory itself bypasses this check

For defense-in-depth consistency, add isPathAllowed(projectPath) validation at the start of followUpFeature:

  async followUpFeature(
    projectPath: string,
@@
  ): Promise<void> {
+    // Validate project path early for defense-in-depth
+    if (!isPathAllowed(projectPath)) {
+      throw new PathNotAllowedError(projectPath);
+    }
+
    const abortController = new AbortController();
🧹 Nitpick comments (18)
apps/ui/tests/utils/navigation/views.ts (1)

40-50: Overly broad exception handling may mask real issues.

The catch block on line 48 silently continues on any error, including network timeouts, selector mismatches, or other unexpected failures. Consider catching only the specific error type expected (e.g., timeout errors), so that other exceptions are properly surfaced:

🔎 Suggested refinement
-  try {
-    const loadingVisible = await loadingElement.isVisible({ timeout: 2000 });
-    if (loadingVisible) {
-      // Wait for loading to disappear (spec view or empty state will appear)
-      await loadingElement.waitFor({ state: "hidden", timeout: 10000 });
-    }
-  } catch {
-    // Loading element not found or already hidden, continue
-  }
+  try {
+    const loadingVisible = await loadingElement.isVisible({ timeout: 2000 });
+    if (loadingVisible) {
+      // Wait for loading to disappear (spec view or empty state will appear)
+      await loadingElement.waitFor({ state: "hidden", timeout: 10000 });
+    }
+  } catch (error) {
+    // Only suppress timeout errors; propagate other failures
+    if (!error.toString().includes("Timeout")) {
+      throw error;
+    }
+    // Loading element not found or already hidden, continue
+  }
apps/ui/tests/utils/views/context.ts (1)

131-131: Consider standardizing element selection across the file.

The file now mixes two approaches for selecting elements by test ID:

  • Direct page.locator('[data-testid="..."]') (lines 11, 21, 29, 131)
  • getByTestId(page, "...") helper (lines 36, 47, 144, 151, 176)

This is particularly evident with context-file selectors:

  • clickContextFile (line 21) uses direct page.locator
  • waitForContextFile (line 131) now uses direct page.locator
  • selectContextFile (line 144) uses getByTestId

Standardizing on one approach (preferably the getByTestId helper for consistency with the imported utility) would improve maintainability and make the codebase more uniform.

🔎 Suggested refactor to use getByTestId consistently
-  const locator = page.locator(`[data-testid="context-file-${filename}"]`);
+  const locator = await getByTestId(page, `context-file-${filename}`);

Note: You may want to apply similar changes to lines 11, 21, and 29 for consistency throughout the file.

apps/ui/src/components/views/welcome-view.tsx (1)

242-259: Good defensive validation before directory creation.

The pre-checks for parent directory existence and type validation provide excellent user feedback and prevent confusing errors. This aligns well with the PR's path security enhancements.

Optional: Consider handling the edge case where stat returns null.

The condition on line 253 (parentStat && !parentStat.isDirectory) will skip the directory type check if stat returns null/undefined. While the subsequent mkdir call will still fail appropriately, you could provide a more specific error message by explicitly handling this case.

🔎 Proposed enhancement
 // Verify parent is actually a directory
 const parentStat = await api.stat(parentDir);
-if (parentStat && !parentStat.isDirectory) {
+if (!parentStat) {
+  toast.error("Cannot access parent directory", {
+    description: `Unable to read directory information: ${parentDir}`,
+  });
+  return;
+}
+if (!parentStat.isDirectory) {
   toast.error("Parent path is not a directory", {
     description: `${parentDir} is not a directory`,
   });
   return;
 }

Note: The theoretical TOCTOU race condition between the existence check and mkdir is acceptable here, as any filesystem changes will be caught by the mkdir operation itself.

apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx (1)

194-196: Consider sanitizing branch names in data-testid attributes.

The dynamic title and aria-label improvements look good. However, the data-testid construction at line 196 uses the raw branch name, which may contain special characters like slashes (e.g., feature/my-branch becomes worktree-branch-feature/my-branch). While Playwright's attribute selectors handle this, it's not ideal and could cause issues with CSS selectors or other test frameworks.

🔎 Proposed refactor to sanitize data-testid
            title={`Click to preview ${worktree.branch}`}
            aria-label={worktree.branch}
-            data-testid={`worktree-branch-${worktree.branch}`}
+            data-testid={`worktree-branch-${worktree.branch.replace(/[^a-zA-Z0-9_-]/g, '-')}`}

And update the test accordingly:

// In the test file
const mainBranchButton = page.locator('[data-testid="worktree-branch-main"]');

Note: If you make this change, ensure all related tests use the same sanitization logic for consistency.

apps/server/src/routes/templates/routes/clone.ts (1)

9-9: Verify if PathNotAllowedError is needed.

PathNotAllowedError is imported but not thrown in the validation blocks (lines 67-82). The code creates plain error responses instead. If this import is unused, consider removing it.

#!/bin/bash
# Description: Check if PathNotAllowedError is used elsewhere in clone.ts

# Search for PathNotAllowedError usage
rg -n 'PathNotAllowedError' apps/server/src/routes/templates/routes/clone.ts
apps/server/src/routes/workspace/routes/directories.ts (1)

24-35: Consider including the path in the error message.

The error message "Workspace directory path does not exist" could be more helpful by including which path was checked.

🔎 Suggested improvement
       res.status(400).json({
         success: false,
-        error: "Workspace directory path does not exist",
+        error: `Workspace directory does not exist: ${resolvedWorkspaceDir}`,
       });
apps/ui/src/lib/project-init.ts (1)

51-77: Consider extracting validation logic into a reusable function.

The project path validation logic (existence check, stat verification, directory check) is well-implemented with clear error messages. However, this validation pattern might be useful elsewhere in the codebase.

🔎 Optional refactor to improve reusability

Extract the validation into a separate function:

+/**
+ * Validates that a path exists and is a directory
+ * @param path - The path to validate
+ * @returns Validation result with error if invalid
+ */
+async function validateProjectDirectory(path: string): Promise<{
+  valid: boolean;
+  error?: string;
+}> {
+  const api = getElectronAPI();
+  
+  const projectExists = await api.exists(path);
+  if (!projectExists) {
+    return {
+      valid: false,
+      error: `Project directory does not exist: ${path}. Create it first before initializing.`,
+    };
+  }
+
+  const projectStat = await api.stat(path);
+  if (!projectStat.success) {
+    return {
+      valid: false,
+      error: projectStat.error || `Failed to stat project directory: ${path}`,
+    };
+  }
+
+  if (projectStat.stats && !projectStat.stats.isDirectory) {
+    return {
+      valid: false,
+      error: `Project path is not a directory: ${path}`,
+    };
+  }
+
+  return { valid: true };
+}
+
 export async function initializeProject(
   projectPath: string
 ): Promise<ProjectInitResult> {
   const api = getElectronAPI();
   const createdFiles: string[] = [];
   const existingFiles: string[] = [];

   try {
-    // Validate that the project directory exists and is a directory
-    const projectExists = await api.exists(projectPath);
-    if (!projectExists) {
-      return {
-        success: false,
-        isNewProject: false,
-        error: `Project directory does not exist: ${projectPath}. Create it first before initializing.`,
-      };
-    }
-
-    // Verify it's actually a directory (not a file)
-    const projectStat = await api.stat(projectPath);
-    if (!projectStat.success) {
-      return {
-        success: false,
-        isNewProject: false,
-        error: projectStat.error || `Failed to stat project directory: ${projectPath}`,
-      };
-    }
-
-    if (projectStat.stats && !projectStat.stats.isDirectory) {
+    const validation = await validateProjectDirectory(projectPath);
+    if (!validation.valid) {
       return {
         success: false,
         isNewProject: false,
-        error: `Project path is not a directory: ${projectPath}`,
+        error: validation.error,
       };
     }
apps/server/src/middleware/validate-paths.ts (1)

36-46: Consider adding type guard for array element validation.

If req.body[actualName] contains an array with non-string elements, validatePath(value) may throw an unexpected error that gets re-thrown rather than handled gracefully.

🔎 Suggested improvement
         // Handle array parameters (paramName[])
         if (paramName.endsWith("[]")) {
           const actualName = paramName.slice(0, -2);
           const values = req.body[actualName];
           if (Array.isArray(values) && values.length > 0) {
             for (const value of values) {
+              if (typeof value === "string") {
                 validatePath(value);
+              }
             }
           }
           continue;
         }
apps/server/src/services/agent-service.ts (1)

65-99: Working directory normalization and validation look correct, with one minor follow‑up

Normalizing effectiveWorkingDirectory via path.resolve and validating it with isPathAllowed before storing in the in‑memory session and metadata is sound and keeps sessions tied to an absolute, policy‑compliant path.

One thing to consider for defense‑in‑depth: in sendMessage, a caller‑provided workingDirectory can temporarily override session.workingDirectory for the Claude agent cwd without an explicit isPathAllowed check here. You’re already validating at the route layer, but adding a guard like this would make the service robust against any future route changes:

Suggested guard in sendMessage for override workingDirectory
  async sendMessage({
     sessionId,
     message,
     workingDirectory,
@@
   }) {
@@
-    // Build SDK options using centralized configuration
+    // Resolve and (optionally) validate working directory override
+    const executionCwd =
+      workingDirectory || session.workingDirectory;
+
+    // Optional defense-in-depth: validate here as well as in route middleware
+    // if (executionCwd && !isPathAllowed(executionCwd)) {
+    //   throw new PathNotAllowedError(executionCwd);
+    // }
+
+    // Build SDK options using centralized configuration
     const sdkOptions = createChatOptions({
-      cwd: workingDirectory || session.workingDirectory,
+      cwd: executionCwd,
@@
       const options: ExecuteOptions = {
@@
-        cwd: workingDirectory || session.workingDirectory,
+        cwd: executionCwd,
apps/ui/src/components/dialogs/file-browser-dialog.tsx (3)

101-152: useCallback‑wrapped recent handlers and browseDirectory are stable and safe

Wrapping handleRemoveRecent and browseDirectory in useCallback with stable dependencies helps avoid needless re-renders and keeps the network call site centralized. The error/warning reset and loading flag handling look correct, and handleSelectRecent reuses browseDirectory cleanly.


166-205: Initial path resolution matches the new workspace config, but the inline priority comment is slightly misleading

The effect correctly prefers:

  • getDefaultWorkspaceDirectory() (which already bakes in ALLOWED_ROOT_DIRECTORY, last project dir, Documents, DATA_DIR),
  • overridden by an explicit initialPath when provided,
  • and finally falls back to “home” when all else fails.

The comment above still mentions a separate “last selected directory from this file browser” step, but that’s now effectively handled inside getDefaultWorkspaceDirectory. Consider updating the comment to mirror the actual order and avoid confusion for future maintainers.


448-466: Keyboard shortcut hint logic is fine but could be slightly more robust

Using navigator.platform?.includes("Mac") behind a typeof navigator !== "undefined" guard is safe and keeps SSR working. If you want to future‑proof this, you could eventually prefer navigator.userAgentData or a more generic check, but it’s not critical for this UI hint.

apps/server/src/services/auto-mode-service.ts (1)

686-722: Consider adding explicit path validation in resumeFeature for defense‑in‑depth

resumeFeature relies on secureFs to enforce path policy when reading agent-output.md, and then calls executeFeature, which validates projectPath/workDir. This is functionally safe, but adding an early isPathAllowed(projectPath) check here would surface configuration mistakes sooner and make the service’s contract clearer.

apps/ui/src/lib/workspace-config.ts (1)

42-99: Default workspace directory resolution matches the documented behavior

The getDefaultWorkspaceDirectory implementation:

  • Prefers workspaceDir when ALLOWED_ROOT_DIRECTORY is configured.
  • Otherwise falls back to last project dir, then Documents/Automaker, then defaultDir from the API.
  • On failure or exceptions, gracefully retries last project dir and Documents before returning null.

This centralizes all path selection logic in one place and works cleanly with the storage abstraction and Electron HTTP bridge.

apps/server/src/lib/security.ts (1)

117-148: isPathWithinDirectory and accessor helpers behave as expected; consider clarifying addAllowedPath’s role

The isPathWithinDirectory implementation using path.relative and checks against ".."/absolute paths is a standard and reliable way to detect containment, and the getAllowedRootDirectory/getDataDirectory/getAllowedPaths accessors make introspection easy.

One subtle point: addAllowedPath only affects what getAllowedPaths() returns; it does not change how isPathAllowed/validatePath decide which paths are permitted (that’s still strictly ALLOWED_ROOT_DIRECTORY + DATA_DIR). If you intend to support additional dynamic roots in the future, a short comment to that effect near addAllowedPath could prevent confusion.

apps/server/src/lib/secure-fs.ts (3)

38-45: Improve type safety by removing as any casts.

The encoding as any casts on lines 44 and 118 bypass TypeScript's type checking. Consider using proper function overloads or typing the encoding parameter more precisely to match the fs.promises signature without type assertions.

🔎 Proposed refactor using proper types

For writeFile:

 export async function writeFile(
   filePath: string,
   data: string | Buffer,
   encoding?: BufferEncoding
 ): Promise<void> {
   const validatedPath = validatePath(filePath);
-  return fs.writeFile(validatedPath, data, encoding as any);
+  return fs.writeFile(validatedPath, data, encoding ? { encoding } : undefined);
 }

For appendFile:

 export async function appendFile(
   filePath: string,
   data: string | Buffer,
   encoding?: BufferEncoding
 ): Promise<void> {
   const validatedPath = validatePath(filePath);
-  return fs.appendFile(validatedPath, data, encoding as any);
+  return fs.appendFile(validatedPath, data, encoding ? { encoding } : undefined);
 }

Also applies to: 112-119


61-67: Use proper return types instead of any.

Lines 66, 72, and 137 use any types where more specific types are available:

  • stat and lstat should return Promise<fs.Stats>
  • readdir options should be typed properly to avoid the as any cast

Using any eliminates type safety benefits for consumers of this module.

🔎 Proposed refactor with proper types

Import the Stats type at the top:

-import fs from "fs/promises";
+import fs, { type Stats } from "fs/promises";

For stat:

-export async function stat(filePath: string): Promise<any> {
+export async function stat(filePath: string): Promise<Stats> {
   const validatedPath = validatePath(filePath);
   return fs.stat(validatedPath);
 }

For lstat:

-export async function lstat(filePath: string): Promise<any> {
+export async function lstat(filePath: string): Promise<Stats> {
   const validatedPath = validatePath(filePath);
   return fs.lstat(validatedPath);
 }

For readdir, consider using proper overloads:

+export async function readdir(
+  dirPath: string
+): Promise<string[]>;
+export async function readdir(
+  dirPath: string,
+  options: { withFileTypes: true; encoding?: BufferEncoding }
+): Promise<fs.Dirent[]>;
+export async function readdir(
+  dirPath: string,
+  options: { withFileTypes?: false; encoding?: BufferEncoding }
+): Promise<string[]>;
 export async function readdir(
   dirPath: string,
   options?: { withFileTypes?: boolean; encoding?: BufferEncoding }
-): Promise<string[] | any[]> {
+): Promise<string[] | fs.Dirent[]> {
   const validatedPath = validatePath(dirPath);
-  return fs.readdir(validatedPath, options as any);
+  return fs.readdir(validatedPath, options as any) as Promise<string[] | fs.Dirent[]>;
 }

Also applies to: 69-75, 133-140


142-156: Consider enhancing documentation for non-validating helpers.

The comments clearly state these functions don't validate, which is good. However, consider making it even more explicit that the returned paths must be validated before use to prevent accidental security bypasses.

🔎 Suggested documentation enhancement
 /**
  * Wrapper around path.join that returns resolved path
- * Does NOT validate - use this for path construction, then pass to other operations
+ * ⚠️ DOES NOT VALIDATE - use only for path construction.
+ * The returned path MUST be passed to a validated operation (e.g., readFile, writeFile)
+ * or explicitly validated via validatePath() before use.
  */
 export function joinPath(...pathSegments: string[]): string {
   return path.join(...pathSegments);
 }

 /**
  * Wrapper around path.resolve that returns resolved path
- * Does NOT validate - use this for path construction, then pass to other operations
+ * ⚠️ DOES NOT VALIDATE - use only for path construction.
+ * The returned path MUST be passed to a validated operation (e.g., readFile, writeFile)
+ * or explicitly validated via validatePath() before use.
  */
 export function resolvePath(...pathSegments: string[]): string {
   return path.resolve(...pathSegments);
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 823e42e and 86d92e6.

📒 Files selected for processing (60)
  • apps/server/.env.example (1 hunks)
  • apps/server/Dockerfile (1 hunks)
  • apps/server/src/lib/automaker-paths.ts (3 hunks)
  • apps/server/src/lib/fs-utils.ts (4 hunks)
  • apps/server/src/lib/image-handler.ts (2 hunks)
  • apps/server/src/lib/secure-fs.ts (1 hunks)
  • apps/server/src/lib/security.ts (1 hunks)
  • apps/server/src/middleware/validate-paths.ts (1 hunks)
  • apps/server/src/routes/agent/index.ts (2 hunks)
  • apps/server/src/routes/agent/routes/send.ts (0 hunks)
  • apps/server/src/routes/agent/routes/start.ts (0 hunks)
  • apps/server/src/routes/auto-mode/index.ts (2 hunks)
  • apps/server/src/routes/features/index.ts (2 hunks)
  • apps/server/src/routes/features/routes/create.ts (0 hunks)
  • apps/server/src/routes/features/routes/list.ts (0 hunks)
  • apps/server/src/routes/fs/routes/browse.ts (3 hunks)
  • apps/server/src/routes/fs/routes/delete.ts (2 hunks)
  • apps/server/src/routes/fs/routes/exists.ts (2 hunks)
  • apps/server/src/routes/fs/routes/mkdir.ts (3 hunks)
  • apps/server/src/routes/fs/routes/read.ts (2 hunks)
  • apps/server/src/routes/fs/routes/readdir.ts (2 hunks)
  • apps/server/src/routes/fs/routes/stat.ts (2 hunks)
  • apps/server/src/routes/fs/routes/write.ts (2 hunks)
  • apps/server/src/routes/git/index.ts (1 hunks)
  • apps/server/src/routes/models/routes/providers.ts (0 hunks)
  • apps/server/src/routes/settings/index.ts (2 hunks)
  • apps/server/src/routes/setup/routes/api-keys.ts (0 hunks)
  • apps/server/src/routes/setup/routes/delete-api-key.ts (1 hunks)
  • apps/server/src/routes/setup/routes/store-api-key.ts (1 hunks)
  • apps/server/src/routes/suggestions/index.ts (1 hunks)
  • apps/server/src/routes/templates/routes/clone.ts (2 hunks)
  • apps/server/src/routes/workspace/routes/config.ts (1 hunks)
  • apps/server/src/routes/workspace/routes/directories.ts (1 hunks)
  • apps/server/src/routes/worktree/index.ts (2 hunks)
  • apps/server/src/services/agent-service.ts (8 hunks)
  • apps/server/src/services/auto-mode-service.ts (27 hunks)
  • apps/server/src/services/feature-loader.ts (15 hunks)
  • apps/server/src/services/settings-service.ts (4 hunks)
  • apps/server/tests/setup.ts (0 hunks)
  • apps/server/tests/unit/lib/security.test.ts (5 hunks)
  • apps/ui/playwright.config.ts (2 hunks)
  • apps/ui/src/components/dialogs/file-browser-dialog.tsx (8 hunks)
  • apps/ui/src/components/new-project-modal.tsx (4 hunks)
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx (1 hunks)
  • apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx (2 hunks)
  • apps/ui/src/components/views/interview-view.tsx (6 hunks)
  • apps/ui/src/components/views/welcome-view.tsx (1 hunks)
  • apps/ui/src/hooks/use-settings-migration.ts (5 hunks)
  • apps/ui/src/lib/electron.ts (3 hunks)
  • apps/ui/src/lib/http-api-client.ts (1 hunks)
  • apps/ui/src/lib/project-init.ts (1 hunks)
  • apps/ui/src/lib/storage.ts (1 hunks)
  • apps/ui/src/lib/workspace-config.ts (1 hunks)
  • apps/ui/src/main.ts (5 hunks)
  • apps/ui/tests/spec-editor-persistence.spec.ts (2 hunks)
  • apps/ui/tests/utils/navigation/views.ts (1 hunks)
  • apps/ui/tests/utils/views/context.ts (1 hunks)
  • apps/ui/tests/worktree-integration.spec.ts (1 hunks)
  • docker-compose.override.yml.example (1 hunks)
  • docker-compose.yml (1 hunks)
💤 Files with no reviewable changes (7)
  • apps/server/src/routes/agent/routes/start.ts
  • apps/server/src/routes/models/routes/providers.ts
  • apps/server/src/routes/setup/routes/api-keys.ts
  • apps/server/src/routes/features/routes/create.ts
  • apps/server/tests/setup.ts
  • apps/server/src/routes/features/routes/list.ts
  • apps/server/src/routes/agent/routes/send.ts
🧰 Additional context used
🧬 Code graph analysis (26)
apps/ui/tests/spec-editor-persistence.spec.ts (1)
apps/ui/tests/utils/views/spec-editor.ts (3)
  • setEditorContent (79-105)
  • getEditorContent (65-74)
  • clickSaveButton (110-122)
apps/server/src/routes/templates/routes/clone.ts (1)
apps/server/src/lib/security.ts (1)
  • isPathAllowed (73-97)
apps/server/src/routes/fs/routes/readdir.ts (2)
apps/server/src/lib/security.ts (1)
  • PathNotAllowedError (11-18)
apps/server/src/routes/common.ts (1)
  • getErrorMessage (370-372)
apps/ui/src/hooks/use-settings-migration.ts (1)
apps/ui/src/lib/storage.ts (2)
  • getItem (21-28)
  • removeItem (51-59)
apps/server/src/routes/fs/routes/stat.ts (2)
apps/server/src/lib/security.ts (1)
  • PathNotAllowedError (11-18)
apps/server/src/routes/common.ts (1)
  • getErrorMessage (370-372)
apps/ui/src/components/new-project-modal.tsx (1)
apps/ui/src/lib/workspace-config.ts (2)
  • getDefaultWorkspaceDirectory (42-99)
  • saveLastProjectDirectory (105-107)
apps/server/src/routes/fs/routes/mkdir.ts (2)
apps/server/src/lib/security.ts (2)
  • isPathAllowed (73-97)
  • PathNotAllowedError (11-18)
apps/server/src/routes/common.ts (1)
  • getErrorMessage (370-372)
apps/server/src/routes/auto-mode/index.ts (8)
apps/server/src/middleware/validate-paths.ts (1)
  • validatePathParams (22-69)
apps/server/src/routes/auto-mode/routes/run-feature.ts (1)
  • createRunFeatureHandler (12-47)
apps/server/src/routes/auto-mode/routes/verify-feature.ts (1)
  • createVerifyFeatureHandler (9-37)
apps/server/src/routes/auto-mode/routes/resume-feature.ts (1)
  • createResumeFeatureHandler (12-43)
apps/server/src/routes/auto-mode/routes/context-exists.ts (1)
  • createContextExistsHandler (9-37)
apps/server/src/routes/auto-mode/routes/analyze-project.ts (1)
  • createAnalyzeProjectHandler (12-35)
apps/server/src/routes/auto-mode/routes/follow-up-feature.ts (1)
  • createFollowUpFeatureHandler (12-59)
apps/server/src/routes/auto-mode/routes/commit-feature.ts (1)
  • createCommitFeatureHandler (9-39)
apps/server/tests/unit/lib/security.test.ts (1)
apps/server/src/lib/security.ts (5)
  • initAllowedPaths (34-56)
  • getAllowedPaths (147-149)
  • getAllowedRootDirectory (133-135)
  • isPathAllowed (73-97)
  • validatePath (103-111)
apps/server/src/routes/workspace/routes/config.ts (1)
apps/server/src/lib/security.ts (3)
  • getAllowedRootDirectory (133-135)
  • getDataDirectory (140-142)
  • addAllowedPath (62-64)
apps/ui/src/components/views/interview-view.tsx (1)
apps/ui/src/lib/workspace-config.ts (2)
  • getDefaultWorkspaceDirectory (42-99)
  • saveLastProjectDirectory (105-107)
apps/server/src/routes/suggestions/index.ts (2)
apps/server/src/middleware/validate-paths.ts (1)
  • validatePathParams (22-69)
apps/server/src/routes/suggestions/routes/generate.ts (1)
  • createGenerateHandler (18-63)
apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx (1)
apps/ui/src/lib/storage.ts (2)
  • getItem (21-28)
  • setItem (36-44)
apps/server/src/routes/worktree/index.ts (2)
apps/server/src/middleware/validate-paths.ts (1)
  • validatePathParams (22-69)
apps/server/src/routes/features/routes/list.ts (1)
  • createListHandler (9-28)
apps/server/src/lib/secure-fs.ts (1)
apps/server/src/lib/security.ts (1)
  • validatePath (103-111)
apps/server/src/routes/fs/routes/browse.ts (2)
apps/server/src/lib/security.ts (2)
  • isPathAllowed (73-97)
  • PathNotAllowedError (11-18)
apps/server/src/routes/common.ts (1)
  • getErrorMessage (370-372)
apps/server/src/routes/agent/index.ts (3)
apps/server/src/middleware/validate-paths.ts (1)
  • validatePathParams (22-69)
apps/server/src/routes/agent/routes/start.ts (1)
  • createStartHandler (11-37)
apps/server/src/routes/agent/routes/send.ts (1)
  • createSendHandler (11-51)
apps/server/src/routes/settings/index.ts (3)
apps/server/src/middleware/validate-paths.ts (1)
  • validatePathParams (22-69)
apps/server/src/routes/settings/routes/get-project.ts (1)
  • createGetProjectHandler (21-45)
apps/server/src/routes/settings/routes/update-project.ts (1)
  • createUpdateProjectHandler (22-60)
apps/server/src/routes/fs/routes/write.ts (2)
apps/server/src/lib/security.ts (1)
  • PathNotAllowedError (11-18)
apps/server/src/routes/common.ts (1)
  • getErrorMessage (370-372)
apps/server/src/services/auto-mode-service.ts (1)
apps/server/src/lib/security.ts (2)
  • isPathAllowed (73-97)
  • PathNotAllowedError (11-18)
apps/ui/src/lib/electron.ts (1)
apps/ui/src/lib/storage.ts (3)
  • getJSON (66-74)
  • setJSON (82-88)
  • removeItem (51-59)
apps/server/src/routes/fs/routes/delete.ts (2)
apps/server/src/lib/security.ts (1)
  • PathNotAllowedError (11-18)
apps/server/src/routes/common.ts (1)
  • getErrorMessage (370-372)
apps/server/src/routes/workspace/routes/directories.ts (1)
apps/server/src/lib/security.ts (2)
  • getAllowedRootDirectory (133-135)
  • addAllowedPath (62-64)
apps/server/src/routes/fs/routes/read.ts (2)
apps/server/src/lib/security.ts (1)
  • PathNotAllowedError (11-18)
apps/server/src/routes/common.ts (1)
  • getErrorMessage (370-372)
apps/ui/src/main.ts (1)
init.mjs (1)
  • __dirname (22-22)
apps/server/src/routes/features/index.ts (6)
apps/server/src/middleware/validate-paths.ts (1)
  • validatePathParams (22-69)
apps/server/src/routes/features/routes/list.ts (1)
  • createListHandler (9-28)
apps/server/src/routes/features/routes/get.ts (1)
  • createGetHandler (9-39)
apps/server/src/routes/features/routes/create.ts (1)
  • createCreateHandler (12-37)
apps/server/src/routes/features/routes/update.ts (1)
  • createUpdateHandler (12-40)
apps/server/src/routes/features/routes/delete.ts (1)
  • createDeleteHandler (9-34)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: e2e
  • GitHub Check: claude-review
  • GitHub Check: e2e
  • GitHub Check: claude-review
🔇 Additional comments (67)
apps/ui/tests/worktree-integration.spec.ts (1)

106-109: Reconsider the selector choice—the timeout increase stems from a step backward in test strategy.

The selector change to data-testid="worktree-branch-main" requires a 50% timeout increase (15s vs 10s), but the very next test in the same file achieves the same result using getByRole("button", { name: "main" }) with only a 10s timeout. Playwright best practices recommend prioritizing user-facing attributes and explicit contracts such as getByRole(), not data-testid. Using data-testid here appears to be a step backward; rather than accepting the longer timeout, either revert to getByRole() or provide justification for why data-testid is necessary in this specific case.

apps/ui/playwright.config.ts (2)

43-43: LGTM - Appropriate testing configuration.

The comment clearly documents that path restrictions are disabled for testing, which is appropriate for the test environment where you need unrestricted file access.


56-57: LGTM - Formatting improvement.

The ternary operator formatting is clear and maintains the same logic.

apps/server/src/routes/templates/routes/clone.ts (2)

66-73: LGTM - Proper path validation.

The parent directory validation is correctly placed after path traversal checks and uses the resolved path. The error message clearly explains the restriction.


75-82: LGTM - Comprehensive path security.

The project path validation prevents directory creation outside the allowed root directory. Good placement before filesystem operations.

docker-compose.override.yml.example (1)

5-10: LGTM - Clearer configuration model.

The shift to a single ALLOWED_ROOT_DIRECTORY simplifies the configuration and the comments clearly explain the usage.

apps/server/.env.example (1)

19-23: LGTM - Comprehensive documentation.

The documentation clearly explains the security implications, recommended usage, and provides a helpful example. The empty default allows unrestricted access for development while making it easy to enable restrictions for production.

apps/server/src/routes/setup/routes/delete-api-key.ts (1)

73-73: LGTM - Consistent error messaging.

The error message correctly reflects the removal of Google provider support and is consistent with the changes in store-api-key.ts.

apps/server/src/routes/workspace/routes/directories.ts (2)

8-22: LGTM - Proper configuration validation.

The validation correctly checks if the root directory is configured and provides a clear error message. The integration with the centralized security model is appropriate.


38-53: LGTM - Comprehensive path registration.

The implementation correctly:

  1. Registers the workspace directory to the allowed paths
  2. Filters and sorts directories appropriately
  3. Registers each discovered directory to the allowed paths for subsequent access

This ensures that discovered directories can be accessed in future requests while maintaining the security boundaries.

docker-compose.yml (1)

40-44: LGTM! Clear security boundary configuration.

The migration from ALLOWED_PROJECT_DIRS to ALLOWED_ROOT_DIRECTORY simplifies the path security model with a single root directory. The comments clearly explain that paths are container-internal, and the default /projects value is appropriate.

apps/ui/tests/spec-editor-persistence.spec.ts (2)

49-50: Good addition for test stability.

Adding an explicit wait for the spec-view to load before accessing the editor improves test reliability by ensuring the view is fully rendered.


86-120: Robust content verification with fallback.

The polling approach with 30-second timeout and fallback to waitForFunction effectively handles CodeMirror's asynchronous content loading after page reload. The try-catch within the polling loop prevents false failures.

apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx (1)

11-11: LGTM! Clean migration to storage abstraction.

The shift from direct localStorage access to the storage module's getItem/setItem functions centralizes storage logic and removes the need for runtime window availability checks.

Also applies to: 95-100

apps/server/src/routes/fs/routes/delete.ts (1)

7-7: LGTM! Consistent security error handling.

The addition of explicit PathNotAllowedError handling with a 403 response aligns with the PR's path security model and matches the pattern used across other filesystem routes.

Also applies to: 25-29

apps/server/src/routes/suggestions/index.ts (1)

7-7: LGTM! Path validation middleware applied correctly.

The validatePathParams("projectPath") middleware integration ensures the /generate route validates paths before executing the handler, consistent with the PR's security model.

Also applies to: 15-15

apps/server/src/routes/fs/routes/write.ts (1)

8-8: LGTM! Consistent security error handling.

The PathNotAllowedError handling with 403 response follows the same pattern as other filesystem routes and properly integrates with the path security system.

Also applies to: 33-37

apps/server/src/routes/fs/routes/stat.ts (1)

7-7: LGTM! Consistent security error handling.

The PathNotAllowedError handling follows the established pattern across filesystem routes, ensuring consistent 403 responses for disallowed paths.

Also applies to: 33-37

apps/server/src/routes/features/index.ts (1)

7-7: LGTM! Path validation middleware successfully integrated.

The middleware correctly validates projectPath before handler execution for all routes that require it. The /agent-output and /generate-title routes appropriately skip validation since they don't use projectPath.

Note: The handlers still perform their own projectPath validation checks (e.g., if (!projectPath)). While this creates some redundancy, it maintains defense-in-depth and doesn't harm correctness.

Also applies to: 19-23

apps/server/src/routes/fs/routes/readdir.ts (1)

7-7: LGTM! Consistent error handling for path security.

The PathNotAllowedError handling is implemented correctly and consistently with other filesystem routes. The 403 response is returned before the generic error handling, ensuring proper HTTP status codes for authorization failures.

Also applies to: 31-35

apps/server/src/routes/fs/routes/read.ts (1)

7-7: LGTM! Path security integrated with existing error handling.

The PathNotAllowedError handling is correctly positioned before the ENOENT checks, ensuring security violations are caught and returned as 403 responses. The existing optional-file logging behavior is preserved.

Also applies to: 42-46

apps/server/src/routes/git/index.ts (1)

6-6: LGTM! Path validation applied to git routes.

Both git routes are correctly wrapped with path validation middleware. The /file-diff route appropriately validates both projectPath and filePath parameters, while /diffs validates only projectPath.

Also applies to: 13-14

apps/server/src/lib/automaker-paths.ts (1)

12-12: LGTM! Filesystem operations migrated to secure wrapper.

The migration from fs/promises to secureFs ensures that all directory creation operations go through path validation. The function signatures and behavior remain unchanged, maintaining backward compatibility while adding security.

Also applies to: 152-152, 214-214

apps/server/src/lib/image-handler.ts (1)

11-11: LGTM! Image reading migrated to secure filesystem.

The migration to secureFs.readFile adds path validation for image operations. The explicit Buffer cast is appropriate since readFile without an encoding parameter returns a Buffer.

Also applies to: 66-66

apps/server/src/routes/fs/routes/mkdir.ts (2)

9-9: LGTM! Path validation enforced before directory creation.

The security checks are well-structured:

  1. Early validation with isPathAllowed prevents unauthorized directory creation
  2. 403 response for forbidden paths with proper error handling
  3. Dynamic tracking with addAllowedPath for created/verified directories

The interaction between isPathAllowed (which checks against ALLOWED_ROOT_DIRECTORY) and addAllowedPath (which tracks specific paths) appears intentional for granular path management.

Also applies to: 24-27, 60-64


34-34: Remove or clarify addAllowedPath call for pre-existing directories.

The function is documented as "Used when dynamically creating new directories within the allowed root" (line 60, security.ts), yet it's called both when a directory already exists (mkdir.ts:34) and when one is newly created (mkdir.ts:56). The allowedPaths Set is not used for actual path validation — isPathAllowed() checks only ALLOWED_ROOT_DIRECTORY and DATA_DIR — making this tracking inconsistent with its stated purpose.

Either remove the call for pre-existing directories or clarify the intent if tracking is needed for other purposes. If the Set becomes unused elsewhere, consider removing addAllowedPath entirely to avoid future maintenance confusion.

apps/server/src/routes/settings/index.ts (1)

17-17: LGTM! Path validation applied to project settings routes.

The middleware is correctly applied only to the project-specific settings endpoints (/project), while global settings and credentials routes appropriately remain unwrapped since they don't use projectPath.

Also applies to: 61-62

apps/server/src/routes/fs/routes/exists.ts (2)

21-26: LGTM! Security validation correctly placed before filesystem access.

The path validation is appropriately positioned after resolution and before any filesystem operations, following the same pattern as other fs route handlers.


34-42: Error handling follows consistent pattern.

The 403 response for PathNotAllowedError is correctly separated from the 500 catch-all, maintaining consistency with other fs routes.

apps/ui/src/lib/http-api-client.ts (1)

765-771: LGTM! Type extension aligns with server-side workspace config changes.

The optional defaultDir field correctly reflects the server's ability to provide a default workspace directory while maintaining backward compatibility.

apps/server/src/routes/fs/routes/browse.ts (2)

108-116: LGTM! Error handling follows consistent pattern.

The 403 response for PathNotAllowedError is correctly separated from other error handling paths.


17-23: Path validation correctly placed and error handling is appropriate.

Validation occurs after path resolution, before file operations. When dirPath is not provided, the handler defaults to os.homedir(), which is then subject to ALLOWED_ROOT_DIRECTORY restrictions if configured. This is intentional behavior—if restrictions are configured, the home directory must fall within the allowed root, or accessing it will correctly return a 403 Forbidden response. The implementation properly enforces these security boundaries.

apps/server/src/routes/agent/index.ts (2)

22-23: LGTM! Path validation middleware correctly applied.

The validatePathParams middleware appropriately validates optional workingDirectory and array imagePaths[] parameters before the handlers execute. The parameter syntax correctly matches the handler signatures.


27-27: No path validation required for /model route.

This route accepts only sessionId and model parameters from the request body for session configuration, not filesystem paths. No path-based operations are performed.

apps/ui/src/hooks/use-settings-migration.ts (2)

23-23: LGTM! Storage abstraction improves resilience.

The getItem and removeItem wrappers from @/lib/storage provide graceful handling when localStorage is unavailable or throws errors, improving reliability across different browser environments.


126-158: Consistent storage abstraction applied throughout migration logic.

All direct localStorage calls have been replaced with the abstracted getItem and removeItem functions, maintaining consistent error handling.

apps/server/src/middleware/validate-paths.ts (2)

22-53: Well-designed reusable middleware with clear parameter syntax.

The middleware correctly handles optional (?) and array ([]) parameter conventions. The documentation examples are helpful.


56-67: LGTM! Error handling correctly distinguishes PathNotAllowedError.

Re-throwing unexpected errors allows Express's error handling middleware to catch them appropriately.

apps/server/src/routes/worktree/index.ts (1)

36-56: LGTM! Path validation middleware applied consistently.

The validatePathParams middleware is correctly applied to all routes that accept path parameters (projectPath, worktreePath, filePath). Routes without path parameters (like /list, /create-pr, /pr-info, /checkout-branch, /switch-branch, /migrate, /stop-dev, /list-dev-servers) appropriately skip validation.

apps/ui/src/main.ts (1)

183-187: Good security practice: conditional path restriction.

The ALLOWED_ROOT_DIRECTORY is now only set when explicitly provided in the environment, allowing unrestricted access by default in development while enabling path restrictions in production/Docker environments. The comment clearly documents this behavior.

apps/server/src/services/settings-service.ts (2)

44-60: Good addition: error propagation after cleanup.

The migration to secureFs is correct. Notably, line 58 now properly throws the error after cleanup, ensuring that atomicWriteJson doesn't silently swallow errors. This improves error handling reliability.


65-76: Type cast required for secureFs compatibility.

The cast to string on line 67 is necessary because secureFs.readFile returns a union type. This is expected behavior given the secure wrapper's type signatures.

apps/ui/src/components/new-project-modal.tsx (2)

85-96: LGTM! Centralized workspace configuration.

The migration from localStorage to workspace-config utilities provides better separation of concerns. Error handling is appropriate with console logging for debugging.


272-303: Verify UX: empty state when no workspace is configured.

Line 289 now shows nothing when workspaceDir is empty, removing the previous "No workspace configured" error message. This might be less alarming for users, but could make it harder to understand why the Browse button needs to be clicked.

Consider if this empty state provides sufficient user guidance, or if a less alarming informational message would improve UX (e.g., "Click Browse to select a directory").

apps/server/src/routes/workspace/routes/config.ts (2)

18-29: LGTM! Proper fallback to DATA_DIR.

When ALLOWED_ROOT_DIRECTORY is not set, the endpoint correctly returns configured: false with DATA_DIR as the default directory. This provides a sensible fallback for unconfigured environments.


32-52: Good security practice: validate before registering path.

The code correctly validates that ALLOWED_ROOT_DIRECTORY exists and is a directory before calling addAllowedPath. This prevents registration of invalid paths in the security allow-list.

apps/server/src/services/feature-loader.ts (1)

7-7: LGTM! Comprehensive migration to secure filesystem.

All filesystem operations throughout the FeatureLoader have been migrated to use the secure-fs wrapper, ensuring consistent path validation across feature management operations. Type casts are appropriately added where the secure wrapper returns union types.

apps/ui/src/components/views/interview-view.tsx (2)

94-121: LGTM! Proper async state management with cleanup.

The useEffect correctly loads the default workspace directory with proper guards:

  • isMounted flag prevents state updates after unmount
  • Line 104 guard prevents overwriting an already-set projectPath
  • Error handling logs issues without crashing

329-335: Good UX: persist directory selection.

Setting initialPath to the current projectPath provides continuity, and calling saveLastProjectDirectory ensures the user's choice is remembered for future sessions.

apps/server/src/lib/fs-utils.ts (2)

5-47: LGTM! Enhanced symlink safety in mkdirSafe.

The migration to secureFs is correct, and the ELOOP handling (lines 29-32) provides better safety when dealing with symlink loops. The console.warn at line 30 aids debugging without throwing errors that could break workflows.


49-67: Verify ELOOP treatment in existsSafe.

Line 63 treats ELOOP as "exists = true", which is documented in the comment but might be counterintuitive. A symlink loop means the path exists but is not usable. Consider if callers of existsSafe expect this behavior, or if ELOOP should throw or return false.

This behavior is intentional per the comments, but verify that all callers of existsSafe can handle the case where the path "exists" but is actually a broken symlink loop and may not be accessible.

apps/server/src/services/agent-service.ts (2)

400-441: secureFs usage for session and metadata persistence is consistent

Replacing raw fs.promises with secureFs.readFile/writeFile and casting the "utf-8" reads to string keeps the I/O consistent with the new path‑validation layer and avoids accidental writes outside allowed roots. The catch‑and‑fallback behavior ([] / {}) on read failures mirrors the previous semantics and should be fine as long as dataDir is mapped under DATA_DIR or ALLOWED_ROOT_DIRECTORY.


545-555: Deleting session files via secureFs aligns with the new security model

Using secureFs.unlink for session file deletion is the right move; it enforces the same path policy as reads/writes and still safely ignores missing files via the surrounding try/catch.

apps/ui/src/components/dialogs/file-browser-dialog.tsx (3)

23-73: Storage abstraction for recent folders is a good improvement

Switching RECENT_FOLDERS_KEY access to getJSON/setJSON and centralizing add/remove logic keeps recent-folder persistence consistent with the new storage layer and safe under SSR (thanks to guards in storage.ts). The de‑duplication and capped history (MAX_RECENT_FOLDERS) logic is also clear.


239-247: Persisting last project directory via saveLastProjectDirectory is aligned with workspace utilities

Calling saveLastProjectDirectory(currentPath) when the user confirms selection ensures the next open will respect this directory via getDefaultWorkspaceDirectory. This ties the dialog behavior neatly into the new workspace-config module.


320-339: Truncating recent folder labels while keeping the full path in the title is a good UX compromise

Using getFolderName plus title={folder} and a truncate span balances readability and discoverability, especially with the new wrapping layout for recent folders.

apps/ui/src/lib/storage.ts (1)

1-100: Centralized storage abstraction is robust and SSR‑safe

The isStorageAvailable guard plus try/catch around all localStorage calls makes this module safe in SSR and privacy‑restricted environments. getJSON/setJSON add a clean, typed way to work with JSON in storage, and the exported storage object gives a convenient aggregate API.

apps/server/src/services/auto-mode-service.ts (3)

489-563: executeFeature’s project/workDir path validation is solid

Validating both projectPath and the resolved workDir with isPathAllowed before any agent execution is exactly what you want for the new ALLOWED_ROOT_DIRECTORY model; it prevents accidental or malicious runs outside the configured boundary while still allowing .automaker worktrees.


1112-1163: loadContextFiles correctly relies on secureFs for enforcement and filters text files

Using secureFs.access/readdir/readFile to pull in .md/.txt files from the resolved context directory keeps these reads within the same security envelope as the rest of the system. The generated system prompt wrapper is also careful to emphasize project‑specific rules and package‑manager usage.


1757-1907: secureFs writes in mock and real agent output paths look correct

In both MOCK mode and the real agent stream:

  • Output paths are derived under workDir or getFeatureDir(projectPath, featureId).
  • Directories are created with secureFs.mkdir(..., { recursive: true }).
  • Writes use secureFs.writeFile so they’re subject to the same path policy.

The debounced writes in runAgent are especially nice for large streams and log errors without crashing the run.

apps/ui/src/lib/workspace-config.ts (2)

18-29: Electron-backed Documents path helper is straightforward and safely logged

getDefaultDocumentsPath correctly delegates to getElectronAPI().getPath("documents") and joins Automaker, with errors logged only when window.console exists. Returning null on failure makes its use in getDefaultWorkspaceDirectory simple.


101-107: saveLastProjectDirectory correctly routes persistence through the storage layer

Using setItem(LAST_PROJECT_DIR_KEY, path) keeps the “last project dir” concept in sync with getDefaultWorkspaceDirectory and avoids direct localStorage usage.

apps/server/tests/unit/lib/security.test.ts (1)

14-51: Security tests comprehensively cover the new ALLOWED_ROOT_DIRECTORY + DATA_DIR behavior

The updated tests for initAllowedPaths, isPathAllowed, validatePath, and getAllowedPaths align well with the new semantics:

  • ALLOWED_ROOT_DIRECTORY and DATA_DIR loading and coexistence.
  • “No restrictions configured” cases where all paths are allowed.
  • Correct handling of relative paths within the allowed root.

This suite should give good confidence that future changes to security.ts won’t accidentally weaken the path model.

Also applies to: 85-137, 139-191, 193-220

apps/ui/src/lib/electron.ts (1)

3-4: Migrating project persistence to storage.getJSON/setJSON is clean and SSR‑friendly

Using getJSON/setJSON for projects and trashed projects, plus removeItem when clearing the current project, removes direct localStorage coupling from this module while keeping behavior identical. The ?? [] fallbacks ensure callers always see arrays even when storage is unavailable.

Also applies to: 2670-2712

apps/server/src/lib/security.ts (2)

8-55: Initialization and error type cleanly define the security boundary

PathNotAllowedError gives a clear, uniform error for policy violations, and initAllowedPaths correctly normalizes ALLOWED_ROOT_DIRECTORY/DATA_DIR and logs their configuration. Treating “no ALLOWED_ROOT_DIRECTORY” as unrestricted (while still honoring DATA_DIR as an exception) preserves backward compatibility.


73-111: isPathAllowed/validatePath implementation is correct and resistant to traversal

isPathAllowed:

  • Always permits paths under DATA_DIR.
  • Falls back to “allow all” when no root is configured.
  • Uses isPathWithinDirectory to ensure paths are truly inside ALLOWED_ROOT_DIRECTORY.

validatePath layering path.resolve and then throwing PathNotAllowedError on failure gives a simple, safe contract for callers.

apps/server/src/lib/secure-fs.ts (1)

1-11: Excellent defense-in-depth security design.

The module provides a clean abstraction layer that enforces path validation at the I/O boundary, ensuring all filesystem operations go through security checks regardless of where they're called from.

Comment on lines 29 to 36
# Install git, curl, and GitHub CLI
RUN apk add --no-cache git curl && \
GH_VERSION=$(curl -s https://api.github.com/repos/cli/cli/releases/latest | grep '"tag_name"' | cut -d '"' -f 4 | sed 's/v//') && \
curl -L "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" -o gh.tar.gz && \
tar -xzf gh.tar.gz && \
mv gh_*_linux_amd64/bin/gh /usr/local/bin/gh && \
rm -rf gh.tar.gz gh_*_linux_amd64

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for usage of gh CLI in the server codebase
rg -n 'gh\b|github-cli' apps/server/src/ --type ts --type js 2>/dev/null || echo "No direct gh command usage found"

# Also check for gh in shell scripts or other files
rg -n '\bgh\b' apps/server/ --type sh 2>/dev/null || echo "No sh usage found"

# Check if there are other references to GitHub API or operations
rg -n 'GitHub|github' apps/server/src/ -i --type ts --type js -c | head -20

Repository: AutoMaker-Org/automaker

Length of output: 6477


🌐 Web query:

alpine github cli package availability

💡 Result:

Yes — Alpine provides GitHub CLI as the package named "github-cli" in the community repository (multiple branches/architectures). Install with:

Package pages / builds: [1][2]. Installation docs: [3].

Sources:
[1] pkgs.alpinelinux.org — github-cli (v3.23 community).
[2] pkgs.alpinelinux.org — github-cli (edge community).
[3] GitHub CLI install (Alpine) — apk add github-cli.


🌐 Web query:

gh cli musl alpine compatibility amd64

💡 Result:

Short answer: Official gh release tarballs on GitHub are built for glibc (linux amd64) — there isn’t an official "linux-musl" amd64 binary from cli/cli. On Alpine you should install the alpine-packaged gh (apk add github-cli from community/stable) or build from source / use the Alpine package (aports). Using the official linux_amd64 tarball on musl-based Alpine may fail unless you add a glibc-compat layer (gcompat/glibc) or run the distro package. [1][2]

Sources:

  • Official gh install notes (mentions Alpine package / edge community repo). [1]
  • gh GitHub releases (linux amd64 assets — no linux-musl asset present). [2]

Critical: Non-deterministic builds, architecture incompatibility, and missing security verification.

The gh CLI is actively used in production code for PR operations (across pr-info.ts, create-pr.ts, and gh-status.ts), making this a critical issue.

The installation block has several critical problems:

  1. Non-deterministic builds: Fetching the latest release means builds are not reproducible and break when new versions are released.
  2. Architecture mismatch: Alpine uses musl libc, but the official linux_amd64 build is glibc-based and incompatible. Without a glibc-compat layer, the binary will fail on Alpine.
  3. No signature/checksum verification: Downloading and executing binaries without verification is a security risk.
  4. Brittle parsing: The grep | cut | sed chain is fragile and breaks with API response changes.
  5. Build failure risk: Network issues or GitHub API downtime will cause builds to fail.

Use Alpine's native package instead:

-# Install git, curl, and GitHub CLI
-RUN apk add --no-cache git curl && \
-    GH_VERSION=$(curl -s https://api.github.com/repos/cli/cli/releases/latest | grep '"tag_name"' | cut -d '"' -f 4 | sed 's/v//') && \
-    curl -L "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" -o gh.tar.gz && \
-    tar -xzf gh.tar.gz && \
-    mv gh_*_linux_amd64/bin/gh /usr/local/bin/gh && \
-    rm -rf gh.tar.gz gh_*_linux_amd64
+# Install git and GitHub CLI from Alpine packages
+RUN apk add --no-cache git github-cli

Alpine's github-cli package is available in the community repository and is the recommended approach for musl-based systems.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Install git, curl, and GitHub CLI
RUN apk add --no-cache git curl && \
GH_VERSION=$(curl -s https://api.github.com/repos/cli/cli/releases/latest | grep '"tag_name"' | cut -d '"' -f 4 | sed 's/v//') && \
curl -L "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" -o gh.tar.gz && \
tar -xzf gh.tar.gz && \
mv gh_*_linux_amd64/bin/gh /usr/local/bin/gh && \
rm -rf gh.tar.gz gh_*_linux_amd64
# Install git and GitHub CLI from Alpine packages
RUN apk add --no-cache git github-cli
🤖 Prompt for AI Agents
In apps/server/Dockerfile around lines 29-36, the current RUN block dynamically
downloads the gh binary from the GitHub releases API using fragile parsing, no
integrity checks, and a glibc-based binary that is incompatible with Alpine;
replace this whole dynamic-download approach with Alpine's native package:
enable the community repository if not already enabled and install github-cli
via apk (optionally pin the apk package version to ensure reproducible builds),
remove the curl/grep/cut/sed/tar/mv steps and any reliance on GitHub API at
build time, and ensure the Dockerfile uses --no-cache and a fixed package list
to keep builds deterministic and compatible with musl.

Comment on lines +39 to 45
} else {
res.status(400).json({
success: false,
error: `Unsupported provider: ${provider}. Only anthropic is supported.`,
});
return;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Search for remaining Google provider references

# Search for Google/google provider references in UI and docs
rg -n -i 'google.*provider|provider.*google' apps/ui/src/ apps/server/src/ --type=ts --type=tsx -C2

# Search for GOOGLE_API_KEY references
rg -n 'GOOGLE_API_KEY' apps/ --type=ts --type=tsx --type=env -C2

Repository: AutoMaker-Org/automaker

Length of output: 122


🏁 Script executed:

#!/bin/bash
# Get more context around the changes in the store-api-key file
cat -n apps/server/src/routes/setup/routes/store-api-key.ts | head -50

# Check for any provider enum or type definitions
rg -n 'provider.*=|type.*Provider|enum.*Provider' apps/server/src/ --type=ts -A2 -B2 | head -50

Repository: AutoMaker-Org/automaker

Length of output: 5306


🏁 Script executed:

#!/bin/bash
# Search for "google" string anywhere in the codebase (case-insensitive)
rg -i 'google' apps/ --max-count=20

# Search for provider configuration or UI files
find apps/ui -name "*provider*" -o -name "*api*" -o -name "*setup*" 2>/dev/null | head -20

# Check for any .env or config files
find apps -name ".env*" -o -name "*config*" -o -name "*.env" 2>/dev/null | head -10

# Search for files mentioning "Unsupported provider" or API key setup UI
rg -i 'unsupported|api.*key.*setup|provider.*select' apps/ui/ --max-count=10

Repository: AutoMaker-Org/automaker

Length of output: 10620


🏁 Script executed:

#!/bin/bash
# Check the api-providers.ts file to see the full Google configuration
sed -n '1,150p' apps/ui/src/config/api-providers.ts

# Check what the settings-view is actually rendering
rg -A 10 "api-providers" apps/ui/src/components/views/settings-view/ --type ts

Repository: AutoMaker-Org/automaker

Length of output: 7051


Backend correctly rejects Google provider; UI already disabled Google support.

The backend implementation correctly restricts the API to only Anthropic. However, the removal is incomplete: the UI already has Google disabled (commented out in api-providers.ts), but orphaned code remains in data models (settings.ts, app-store.ts), state management (use-api-key-management.ts), endpoint documentation comments, and test fixtures. These should be cleaned up to avoid confusion about provider support.

Remaining cleanup:

  • Remove commented Google provider config in api-providers.ts
  • Update endpoint documentation in update-credentials.ts and get-credentials.ts
  • Remove google field from settings type definition
  • Remove Google key references from test fixtures
  • Clean up unused state variables in use-api-key-management.ts
🤖 Prompt for AI Agents
In apps/server/src/routes/setup/routes/store-api-key.ts lines 39-45 and related
files, the backend already restricts providers to Anthropic but leftover
Google-related code and comments remain elsewhere; remove the commented Google
provider entry in api-providers.ts, update endpoint documentation comments in
update-credentials.ts and get-credentials.ts to reflect only Anthropic, remove
the google field from the settings type in settings.ts and any Google-related
fields in app-store.ts, delete Google key references from test fixtures, and
prune unused state variables and handlers in use-api-key-management.ts that
reference Google so the codebase consistently reflects only Anthropic support.

Comment on lines +52 to +58
// Wait for either the main spec view or empty state to be visible
// The spec-view element appears when loading is complete and spec exists
// The spec-view-empty element appears when loading is complete and spec doesn't exist
await Promise.race([
waitForElement(page, "spec-view", { timeout: 10000 }).catch(() => null),
waitForElement(page, "spec-view-empty", { timeout: 10000 }).catch(() => null),
]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Risk of silent test failure with Promise.race.

If both waitForElement calls fail, Promise.race will resolve with [null, null] rather than rejecting. The test will continue without verifying that either spec-view or spec-view-empty actually appeared, potentially masking real issues where the page fails to load properly.

Consider asserting that at least one element was found:

🔎 Proposed fix
-  // Wait for either the main spec view or empty state to be visible
-  // The spec-view element appears when loading is complete and spec exists
-  // The spec-view-empty element appears when loading is complete and spec doesn't exist
-  await Promise.race([
-    waitForElement(page, "spec-view", { timeout: 10000 }).catch(() => null),
-    waitForElement(page, "spec-view-empty", { timeout: 10000 }).catch(() => null),
-  ]);
+  // Wait for either the main spec view or empty state to be visible
+  // The spec-view element appears when loading is complete and spec exists
+  // The spec-view-empty element appears when loading is complete and spec doesn't exist
+  const results = await Promise.race([
+    waitForElement(page, "spec-view", { timeout: 10000 }).catch(() => null),
+    waitForElement(page, "spec-view-empty", { timeout: 10000 }).catch(() => null),
+  ]);
+  if (!results) {
+    throw new Error(
+      "Spec view failed to load: neither spec-view nor spec-view-empty element appeared"
+    );
+  }
🤖 Prompt for AI Agents
In apps/ui/tests/utils/navigation/views.ts around lines 52 to 58, the
Promise.race approach can silently allow both waits to fail and let the test
continue; change the logic to explicitly verify that at least one element was
found (e.g., use Promise.allSettled or Promise.any and then assert the result)
and throw or fail the test if both waitForElement calls returned null/failed so
the test does not proceed when neither spec-view nor spec-view-empty appeared.

@webdevcody webdevcody added Testers-Requested Request for others to test an enhancement or bug fix/etc. Do Not Merge Use this label if something should not be merged. labels Dec 21, 2025
This commit introduces several improvements to the security and file handling mechanisms across the application. Key changes include:

- Updated the Dockerfile to pin the GitHub CLI version for reproducible builds.
- Refactored the secure file system operations to ensure consistent path validation and type handling.
- Removed legacy path management functions and streamlined the allowed paths logic in the security module.
- Enhanced route handlers to validate path parameters against the ALLOWED_ROOT_DIRECTORY, improving security against unauthorized access.
- Updated the settings service to focus solely on the Anthropic API key, removing references to Google and OpenAI keys.

These changes aim to enhance security, maintainability, and clarity in the codebase.

Tests: All unit tests passing.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/server/src/services/settings-service.ts (2)

65-76: Add TypeScript overloads to secureFs.readFile for proper type narrowing.

The readFile function in secure-fs.ts lacks proper overloads and always returns Promise<string | Buffer>. When an encoding is provided, TypeScript cannot narrow the return type to string, necessitating the as string cast. Instead of adding runtime type guards, add overloads to match Node.js fs behavior:

export async function readFile(filePath: string): Promise<Buffer>;
export async function readFile(filePath: string, encoding: BufferEncoding): Promise<string>;
export async function readFile(filePath: string, encoding?: BufferEncoding): Promise<string | Buffer> {
  // existing implementation
}

This eliminates the need for casts throughout the codebase and provides compile-time type safety.


495-506: Confirm intentional removal of Google/OpenAI API key support and address type mismatch.

The credentials migration intentionally removes support for Google and OpenAI API keys—only Anthropic keys are migrated. This is evident from the Credentials interface in apps/server/src/types/settings.ts, which only defines apiKeys.anthropic, and the commented-out Google provider config in apps/ui/src/config/api-providers.ts (lines 93–99).

However, there's a type inconsistency: apps/ui/src/components/views/settings-view/api-keys/hooks/use-api-key-management.ts still references apiKeys.google (lines 25, 48), which will now be undefined post-migration, potentially causing runtime errors or type violations.

Ensure this removal aligns with PR objectives and consider:

  • Removing the unused googleKey references from the UI hook or updating the Credentials type to explicitly exclude these fields
  • Adding a deprecation notice or migration guide for existing users
  • Clarifying in release notes that Google/OpenAI keys will not be preserved
♻️ Duplicate comments (2)
apps/server/src/routes/auto-mode/index.ts (1)

21-37: Consistent path validation applied across all routes.

The previous review concern about inconsistent path validation has been addressed. All routes that accept filesystem path parameters now have appropriate validatePathParams middleware, with correct handling of optional (?) and array ([]) parameters.

apps/server/src/services/auto-mode-service.ts (1)

1585-1598: Type safety with withFileTypes option.

When using withFileTypes: true, secureFs.readdir should return Dirent[]. As noted in a previous review, ensure the secure-fs wrapper is properly typed with overloads, or add an explicit type annotation here for clarity:

const entries = await secureFs.readdir(featuresDir, { withFileTypes: true }) as import('fs').Dirent[];
🧹 Nitpick comments (2)
apps/server/src/routes/workspace/routes/directories.ts (1)

28-45: Consider using secureFs for consistency.

While reading from allowedRootDirectory is inherently safe (it's the root of allowed paths), using secureFs would maintain consistency with the pattern used in other service files. This is optional since the current implementation is correct.

apps/server/src/routes/templates/routes/clone.ts (1)

9-9: PathNotAllowedError is imported but unused.

The code uses inline 403 responses (lines 68-72, 77-81) instead of throwing PathNotAllowedError. Either remove the unused import or refactor to throw the error for consistency with mkdir.ts.

🔎 Option 1: Remove unused import
-import { isPathAllowed, PathNotAllowedError } from "../../../lib/security.js";
+import { isPathAllowed } from "../../../lib/security.js";
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 86d92e6 and 9cf12b9.

📒 Files selected for processing (20)
  • apps/server/Dockerfile (1 hunks)
  • apps/server/src/lib/secure-fs.ts (1 hunks)
  • apps/server/src/lib/security.ts (1 hunks)
  • apps/server/src/routes/auto-mode/index.ts (2 hunks)
  • apps/server/src/routes/fs/routes/browse.ts (2 hunks)
  • apps/server/src/routes/fs/routes/mkdir.ts (3 hunks)
  • apps/server/src/routes/fs/routes/resolve-directory.ts (0 hunks)
  • apps/server/src/routes/fs/routes/save-board-background.ts (0 hunks)
  • apps/server/src/routes/fs/routes/save-image.ts (0 hunks)
  • apps/server/src/routes/fs/routes/validate-path.ts (1 hunks)
  • apps/server/src/routes/settings/routes/get-credentials.ts (1 hunks)
  • apps/server/src/routes/settings/routes/update-credentials.ts (1 hunks)
  • apps/server/src/routes/templates/routes/clone.ts (2 hunks)
  • apps/server/src/routes/workspace/routes/config.ts (1 hunks)
  • apps/server/src/routes/workspace/routes/directories.ts (1 hunks)
  • apps/server/src/services/auto-mode-service.ts (27 hunks)
  • apps/server/src/services/settings-service.ts (4 hunks)
  • apps/server/src/types/settings.ts (0 hunks)
  • apps/server/tests/unit/lib/security.test.ts (3 hunks)
  • apps/server/tests/unit/services/settings-service.test.ts (2 hunks)
💤 Files with no reviewable changes (4)
  • apps/server/src/routes/fs/routes/save-board-background.ts
  • apps/server/src/types/settings.ts
  • apps/server/src/routes/fs/routes/resolve-directory.ts
  • apps/server/src/routes/fs/routes/save-image.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/server/src/routes/settings/routes/get-credentials.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/server/src/routes/fs/routes/browse.ts
🧰 Additional context used
🧬 Code graph analysis (7)
apps/server/src/routes/workspace/routes/directories.ts (1)
apps/server/src/lib/security.ts (1)
  • getAllowedRootDirectory (120-122)
apps/server/src/routes/auto-mode/index.ts (4)
apps/server/src/middleware/validate-paths.ts (1)
  • validatePathParams (22-69)
apps/server/src/routes/auto-mode/routes/verify-feature.ts (1)
  • createVerifyFeatureHandler (9-37)
apps/server/src/routes/auto-mode/routes/context-exists.ts (1)
  • createContextExistsHandler (9-37)
apps/server/src/routes/auto-mode/routes/commit-feature.ts (1)
  • createCommitFeatureHandler (9-39)
apps/server/src/services/auto-mode-service.ts (1)
apps/server/src/lib/security.ts (2)
  • isPathAllowed (60-84)
  • PathNotAllowedError (11-18)
apps/server/src/routes/fs/routes/mkdir.ts (2)
apps/server/src/lib/security.ts (2)
  • isPathAllowed (60-84)
  • PathNotAllowedError (11-18)
apps/server/src/routes/common.ts (1)
  • getErrorMessage (370-372)
apps/server/tests/unit/lib/security.test.ts (1)
apps/server/src/lib/security.ts (5)
  • initAllowedPaths (31-51)
  • getAllowedPaths (134-143)
  • getAllowedRootDirectory (120-122)
  • isPathAllowed (60-84)
  • validatePath (90-98)
apps/server/src/routes/templates/routes/clone.ts (1)
apps/server/src/lib/security.ts (1)
  • isPathAllowed (60-84)
apps/server/src/lib/secure-fs.ts (1)
apps/server/src/lib/security.ts (1)
  • validatePath (90-98)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: claude-review
  • GitHub Check: e2e
🔇 Additional comments (30)
apps/server/src/services/auto-mode-service.ts (4)

490-493: Path validation correctly guards feature execution.

Good defensive check at the entry point of feature execution. Validating projectPath early prevents downstream operations on disallowed paths.


558-561: Validates derived working directory separately.

Good practice to validate workDir after deriving it from worktreePath or projectPath, as worktrees could potentially be located outside allowed directories.


1946-1949: Error message updated to reflect supported authentication.

The error message correctly references only ANTHROPIC_API_KEY and claude login, aligning with the removal of Google/OpenAI API key support from the codebase.


17-17: Comprehensive migration to secure filesystem wrapper.

The service consistently uses secureFs for all file I/O operations (access, readFile, writeFile, mkdir, copyFile, readdir), ensuring path validation is enforced at the filesystem layer.

apps/server/src/routes/settings/routes/update-credentials.ts (1)

1-9: Documentation accurately reflects Anthropic-only credential support.

The updated documentation correctly describes the narrowed scope of this endpoint, aligning with the removal of Google and OpenAI API key support from the codebase.

apps/server/src/routes/fs/routes/validate-path.ts (2)

8-8: Removed dynamic path allowlisting in favor of static configuration.

The endpoint now only checks isPathAllowed without adding paths to an allowed list. This aligns with the centralized ALLOWED_ROOT_DIRECTORY approach where allowed paths are defined by configuration rather than dynamically added at runtime.


24-38: Appropriate use of raw fs.stat for path validation.

Using fs.stat directly (rather than secureFs) is correct here since this endpoint's purpose is to validate whether a path should be allowed. The actual security boundary is provided by isPathAllowed() at line 37, which returns the authorization status without performing side effects.

apps/server/src/routes/workspace/routes/directories.ts (1)

14-22: Enforces ALLOWED_ROOT_DIRECTORY configuration requirement.

Good validation that returns a clear error message when the root directory is not configured. This ensures the workspace directories endpoint only operates within explicitly defined boundaries.

apps/server/src/routes/workspace/routes/config.ts (1)

14-59: LGTM!

The handler correctly uses the centralized security utilities to retrieve and validate the workspace configuration. The direct fs.stat usage is appropriate here since it's validating the server's own configured directory, not user-supplied paths.

apps/server/src/routes/fs/routes/mkdir.ts (2)

22-27: LGTM!

Path validation is correctly placed after resolution but before any filesystem operations, ensuring unauthorized paths are rejected early.


55-60: LGTM!

The PathNotAllowedError is properly caught and returns a 403 status, consistent with the security model.

apps/server/src/routes/templates/routes/clone.ts (1)

66-82: LGTM on the validation logic.

Both parent directory and project path are correctly validated against the allowed paths before proceeding with the clone operation. The dual validation ensures neither the parent nor the target path can escape the security boundary.

apps/server/tests/unit/services/settings-service.test.ts (2)

250-269: LGTM!

Test correctly updated to reflect the anthropic-only API key structure while maintaining verification of the deep merge behavior.


310-316: LGTM!

Test updated to use the simplified credentials structure.

apps/server/src/lib/security.ts (4)

11-18: LGTM!

Well-structured custom error class with a descriptive message that helps users understand why their request was rejected and what the constraints are.


31-51: LGTM!

Environment-based initialization with clear logging. The console output provides good visibility into the security configuration state at startup.


60-84: LGTM!

The access control logic is well-structured with clear priority:

  1. Always allow DATA_DIR (necessary for settings/credentials)
  2. Allow all when no restriction is configured (backward compatibility)
  3. Enforce ALLOWED_ROOT_DIRECTORY boundary when set

104-115: Path containment check is correct.

The path.relative approach properly handles path traversal:

  • Returns .. prefix when target escapes the directory
  • Returns absolute path if on different drives (Windows)
  • Returns empty string or relative path when contained
apps/server/tests/unit/lib/security.test.ts (4)

14-51: LGTM! Comprehensive test coverage for the new ALLOWED_ROOT_DIRECTORY configuration.

The test suite properly covers:

  • Loading ALLOWED_ROOT_DIRECTORY and verifying via getAllowedRootDirectory()
  • DATA_DIR exception handling
  • Combined ALLOWED_ROOT_DIRECTORY + DATA_DIR scenarios
  • Proper path resolution

The tests align well with the security module's new behavior.


54-106: LGTM! Excellent test coverage for isPathAllowed behavior.

The tests correctly verify:

  • Paths within ALLOWED_ROOT_DIRECTORY are allowed
  • Paths outside ALLOWED_ROOT_DIRECTORY are denied
  • All paths are allowed when no restrictions are configured
  • DATA_DIR doesn't restrict other paths when ALLOWED_ROOT_DIRECTORY is not set

This properly exercises the new security boundary logic.


108-160: LGTM! validatePath tests cover critical security scenarios.

The tests properly verify:

  • Allowed paths return resolved paths
  • Disallowed paths throw errors
  • No restrictions allow all paths
  • Relative path resolution works correctly within allowed directory

162-189: LGTM! getAllowedPaths tests verify correct array composition.

Tests confirm that getAllowedPaths returns an array containing both ALLOWED_ROOT_DIRECTORY and DATA_DIR when configured, with proper path resolution.

apps/server/src/services/settings-service.ts (3)

10-10: LGTM! Secure filesystem integration.

The migration from direct fs/promises to secureFs enforces path validation at the I/O layer, providing defense-in-depth security.


44-60: LGTM! Proper atomic write implementation with secure-fs.

The atomic write pattern (write-to-temp-then-rename) is correctly implemented using secureFs operations, and the error is properly rethrown after cleanup attempt.


270-286: Intentional refactor to focus solely on Anthropic credentials.

This change is part of a deliberate refactor (commit 9cf12b9) to remove support for Google and OpenAI keys and focus solely on Anthropic. All callers in the codebase—get-credentials.ts and update-credentials.ts—have already been updated to work with the new signature, and their documentation reflects that only anthropic is returned. No code is accessing the removed properties.

apps/server/src/lib/secure-fs.ts (5)

1-12: LGTM! Excellent security pattern with clear documentation.

The secure filesystem adapter enforces ALLOWED_ROOT_DIRECTORY restrictions at the I/O layer, providing defense-in-depth security. The documentation clearly explains the purpose and approach.


17-46: LGTM! Type-safe wrapper implementations.

The access, readFile, and writeFile wrappers properly handle optional encoding parameters in a type-safe manner. The past review comments about as any casts have been addressed - the current implementation correctly passes encoding conditionally without type casts.


62-79: LGTM! Proper function overloads for readdir.

The function overloads correctly handle the withFileTypes option and properly distinguish between returning string[] vs Dirent[]. The past review comment about type safety has been addressed.


111-119: LGTM! Dual path validation for copy and rename operations.

Both copyFile and rename correctly validate both source and destination paths before performing the operation, ensuring security boundaries are enforced for both paths.

Also applies to: 136-143


154-168: LGTM! Clear documentation for non-validating path helpers.

The joinPath and resolvePath helpers are clearly documented as NOT performing validation. This is appropriate since these are for path construction prior to validation by other operations.

Comment on lines +29 to +35
# Install git, curl, and GitHub CLI (pinned version for reproducible builds)
RUN apk add --no-cache git curl && \
GH_VERSION="2.63.2" && \
curl -L "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" -o gh.tar.gz && \
tar -xzf gh.tar.gz && \
mv "gh_${GH_VERSION}_linux_amd64/bin/gh" /usr/local/bin/gh && \
rm -rf gh.tar.gz "gh_${GH_VERSION}_linux_amd64"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

CRITICAL: glibc binary incompatible with Alpine's musl libc.

While pinning the version to 2.63.2 addresses build reproducibility, the critical architecture incompatibility flagged in previous reviews remains unresolved. The official linux_amd64 gh CLI binary is compiled against glibc, but Alpine Linux uses musl libc. This binary will fail at runtime on Alpine without a glibc compatibility layer (which is not installed).

Since the gh CLI is actively used in production code (as confirmed in previous reviews), this will cause runtime failures when the application attempts to execute gh commands.

The recommended solution is to use Alpine's native github-cli package from the community repository:

-# Install git, curl, and GitHub CLI (pinned version for reproducible builds)
-RUN apk add --no-cache git curl && \
-    GH_VERSION="2.63.2" && \
-    curl -L "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" -o gh.tar.gz && \
-    tar -xzf gh.tar.gz && \
-    mv "gh_${GH_VERSION}_linux_amd64/bin/gh" /usr/local/bin/gh && \
-    rm -rf gh.tar.gz "gh_${GH_VERSION}_linux_amd64"
+# Install git and GitHub CLI from Alpine packages
+RUN apk add --no-cache git github-cli

This approach:

  • Uses musl-compatible binaries built specifically for Alpine
  • Eliminates download/extraction complexity
  • Removes network dependency during build
  • Provides package-managed updates and security patches

If version pinning is required for reproducibility, you can pin the Alpine package version: apk add --no-cache git github-cli=2.63.2-r0 (verify exact package version in Alpine repos).

Additional concern: The current approach also lacks checksum or signature verification, creating a supply-chain security risk.

To verify the gh CLI functionality and Alpine package availability, run:

#!/bin/bash
# Verify gh CLI is used in the codebase
echo "=== Checking gh CLI usage in server code ==="
rg -n '\bgh\b' apps/server/src/ --type ts -C2 | head -50

# Check Alpine package availability and versions
echo -e "\n=== Checking Alpine github-cli package availability ==="
echo "Testing with Alpine container..."
docker run --rm alpine:3.19 sh -c 'apk update && apk search github-cli'
🤖 Prompt for AI Agents
In apps/server/Dockerfile around lines 29-35, the Dockerfile downloads the
upstream linux_amd64 gh tarball (glibc-linked) which is incompatible with
Alpine's musl and lacks checksum verification; replace that block to install the
Alpine-native github-cli package via apk (e.g., apk add --no-cache git
github-cli) instead of downloading/extracting the binary, optionally pin the
package version to a specific Alpine package release if reproducible builds are
required (verify the exact version in the Alpine repo before pinning), remove
the curl/tar/mv/rm steps and any temporary files, and ensure supply-chain safety
by preferring the package manager or adding checksum/signature verification if
you must download an external binary.

Comment on lines +84 to +87
export async function stat(filePath: string): Promise<any> {
const validatedPath = validatePath(filePath);
return fs.stat(validatedPath);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Use proper return types instead of any.

Both stat and lstat return any, which bypasses TypeScript's type safety. These should return the proper type from fs/promises:

🔎 Proposed fix for type safety
+import type { Stats } from "fs";

 /**
  * Wrapper around fs.stat that validates path first
  */
-export async function stat(filePath: string): Promise<any> {
+export async function stat(filePath: string): Promise<Stats> {
   const validatedPath = validatePath(filePath);
   return fs.stat(validatedPath);
 }

 /**
  * Wrapper around fs.lstat that validates path first
  * Returns file stats without following symbolic links
  */
-export async function lstat(filePath: string): Promise<any> {
+export async function lstat(filePath: string): Promise<Stats> {
   const validatedPath = validatePath(filePath);
   return fs.lstat(validatedPath);
 }

Also applies to: 149-152

🤖 Prompt for AI Agents
In apps/server/src/lib/secure-fs.ts around lines 84-87 (and also for lstat at
149-152), the functions return Promise<any>; change them to return
Promise<Stats> by importing the proper type (import type { Stats } from 'fs')
and updating the function signatures to stat(filePath: string): Promise<Stats>
and lstat(filePath: string): Promise<Stats>; keep the existing validatePath and
fs.promises calls but replace any with the Stats type to restore TypeScript type
safety.

@claude
Copy link

claude bot commented Dec 21, 2025

PR Review: Docker Security Improvements

I have completed a comprehensive review of this PR. This is a well-architected security enhancement that introduces defense-in-depth path validation. Overall, the changes are solid, but I have identified several important issues that should be addressed.

Strengths

1. Excellent Defense-in-Depth Architecture

  • The secure-fs.ts wrapper is a brilliant design pattern that enforces security at the filesystem layer
  • Provides type-safe wrappers for all fs operations with consistent validation
  • Makes it nearly impossible for future code to accidentally bypass security checks

2. Clean Middleware Design

  • validatePathParams middleware is well-designed with excellent DX
  • Support for optional and array parameters is thoughtful
  • Proper error handling with 403 status codes

3. Good Test Coverage

  • Security tests cover critical scenarios
  • Tests verify both allowed and denied paths
  • Confirms backward compatibility

CRITICAL: Incomplete Migration to secure-fs

Problem: Many files still use direct fs imports instead of secure-fs, creating security bypass opportunities.

Found 30+ files that import fs directly, including:

  • apps/server/src/routes/fs/routes/write.ts:6
  • apps/server/src/routes/workspace/routes/directories.ts:6
  • apps/server/src/routes/templates/routes/clone.ts:8

Why this matters:

  1. Inconsistent enforcement at different layers
  2. Future regression risk
  3. Defeats the defense-in-depth principle

Recommendation: Migrate all files to use secure-fs for true defense-in-depth.

HIGH: Redundant Validation in clone.ts

Location: apps/server/src/routes/templates/routes/clone.ts:52-64

The custom path traversal check is redundant since isPathAllowed already handles this. Two sources of truth is error-prone.

Recommendation: Remove custom check, rely on isPathAllowed.

MEDIUM Issues

Inconsistent Error Handling

Some routes use middleware validation, others use inline. This creates confusion.

Recommendation: Standardize on middleware approach and document exceptions.

Missing Test Cases

  • No tests for PathNotAllowedError handling
  • No middleware tests for optional/array parameters
  • Missing edge case tests

Type Safety in secure-fs.ts

Using Promise at lines 84 and 149 defeats TypeScript benefits.

Recommendation: Use Promise from fs module.

Security Analysis

Strengths:

  • Correct path traversal protection
  • Proper 403 responses
  • No command injection vulnerabilities
  • Docker runs as non-root user
  • Pinned dependency versions

Concerns:

  1. Incomplete migration creates bypass opportunities (CRITICAL)
  2. No symlink validation
  3. Minor TOCTOU race conditions (low impact)

Final Recommendations

Must Fix Before Merge:

  1. Migrate all routes to secure-fs (CRITICAL)
  2. Remove redundant validation in clone.ts (HIGH)

Should Fix Before Merge:

  1. Standardize validation approach
  2. Fix any types in secure-fs.ts
  3. Add middleware tests

Consider for Future:

  1. Symlink validation
  2. Performance profiling
  3. Integration tests

Verdict

Rating: 7/10 - Good foundation, needs completion

The core design is excellent, but the incomplete migration from fs to secure-fs undermines the security improvements.

Recommendation: Request changes to address CRITICAL and HIGH issues before merging.

Great work on the architecture! Once the migration is complete, this will significantly improve security.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Do Not Merge Use this label if something should not be merged. Testers-Requested Request for others to test an enhancement or bug fix/etc.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants