Skip to content

feat(harness): 앱 관리 하네스 동기화 + 컴포넌트 토글#44

Merged
dong-park merged 7 commits into
mainfrom
feat/harness-sync
Jun 17, 2026
Merged

feat(harness): 앱 관리 하네스 동기화 + 컴포넌트 토글#44
dong-park merged 7 commits into
mainfrom
feat/harness-sync

Conversation

@dong-park

@dong-park dong-park commented Jun 17, 2026

Copy link
Copy Markdown
Owner

무엇을

GUI 앱이 Claude 하네스(~/.claude의 CLAUDE.md 블록 + skills/hooks/commands 심링크)를 자체적으로 동기화한다. install.sh 수동 재실행 없이 앱 설치/재시작만으로 하네스가 repo 소스와 정렬되고, 컴포넌트를 개별로 켜고 끌 수 있다.

Claude 네이티브 플러그인은 always-on 지침(CLAUDE.md)을 못 실어준다. 앱은 fs 권한이 있어 그 갭을 메울 수 있다 — 앱이 곧 하네스 배포 채널.

변경 (T1~T5)

  • T1 harness.rs 순수 코어: 마커 안전 inject/remove, VERSION 게이트, config, compute_claude_md (단위테스트 11)
  • T2 IO 레이어 + Tauri 커맨드 3개 (harness_status/sync_harness/set_component_enabled), 전부 spawn_blocking
  • T3 런치훅: 시작 시 version-gate 백그라운드 자동 동기화 (시작 안 막음, repo 없으면 graceful skip)
  • T4 Settings → Harness UI: 컴포넌트별 토글 + 버전/링크 상태 + 재동기화 + "자동관리 영역" 고지
  • T5 install.sh/README: 부트스트랩은 유지, ongoing sync는 앱 담당 명시
  • IO 통합테스트: temp HOME+repo로 apply/심링크 backup가드/toggle 실측 (11→12)

안전장치

  • 마커 안만 재작성 — 마커 밖 사용자 콘텐츠 불가침
  • 심링크 backup 가드: 사용자 실디렉터리는 .pre-pharos-*로 백업, remove_dir_all 안 씀
  • 원자적 쓰기(temp+rename), 네트워크 0(repo 소스), repo 없으면 graceful

검증

  • 단위+IO 통합 테스트 12/12, yarn check 전체 그린 (vitest 588)
  • 실앱 입증: 새 빌드 /Applications 교체+재시작 → harness.json fable:false로 런치훅이 FABLE만 제거(297→277), 복원 시 재주입(version-gate가 guidelines 1.2.0 최신소스 반영)
  • 잔여: React 버튼 onClick 자체는 tsc만(메인앱 UI는 MCP로 못 몲) — 단 커맨드 로직은 launch 경로로 실증

비목표 (연기)

per-skill 토글, 원격 마켓플레이스, 크로스머신 번들 전파

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Harness section in Settings to manage component synchronization
    • Automatic harness component updates trigger on app launch
    • Per-component enable/disable toggles for guidelines, pharos, fable, skills, hooks, and commands
    • Manual sync action available in Settings
  • Documentation

    • Updated documentation on automatic harness synchronization behavior after initial installation

dong-park and others added 6 commits June 17, 2026 15:13
~/.claude 하네스를 repo 소스 블록과 정렬하는 apply primitive의 순수 부분.
fs/Tauri 의존 없이 마커 안전성·멱등성·사용자 콘텐츠 보존을 단위테스트로 봉쇄.

- inject_block/remove_block: 마커 사이만 교체/제거 (install.sh inject_marker Rust 포팅)
- block_version/needs_update: VERSION 게이트
- compute_claude_md: enable=주입·disable=제거, 멱등
- HarnessConfig: 컴포넌트별 on/off, ~/.config/pharos/harness.json (기본 전부 on)
- 단위테스트 11개 (멱등·마커밖 보존·stale 교체·disable 제거·config 라운드트립)

IO/Tauri command·런치훅·UI는 T2~T5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
순수 코어를 감싸 파일시스템/설정/커맨드를 붙인다.
- repo_dir(PHAROS_DIR or ~/pharos)·claude_md_path·config_path
- HarnessConfig 컴포넌트별 enabled 조회/설정 + ComponentStatus
- apply_harness: 활성 블록 주입/비활성 제거(원자적 쓰기), 유닛 심링크
  보장/제거(install.sh link() 시맨틱 재현 — 실제 파일/디렉터리는 백업)
- harness_status/sync_harness/set_component_enabled 커맨드(spawn_blocking)
- lib.rs generate_handler! 등록

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lib.rs .setup()에서 daemon ensure 직후 spawn_blocking로 자동 적용.
- needs_sync: 활성 블록 버전 불일치/누락, 비활성 블록 잔존, 유닛 심링크
  존재 여부 불일치 중 하나라도 있으면 동기화 필요로 판정
- auto_sync_on_launch: 드리프트 있을 때만 apply_harness 호출(정상이면
  no-op), repo 없으면 조용히 skip, 시작을 막지 않음
- T2에서 needs_update에 달았던 임시 allow(dead_code) 제거(이제 사용됨)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
마운트 시 harness_status로 컴포넌트 상태 로드.
- 컴포넌트별 행: 이름 + 버전(블록 applied→source) 또는 링크 상태(유닛)
  + on/off 토글, 토글 시 set_component_enabled 후 상태 갱신
- "지금 재동기화" 버튼 → sync_harness
- 상단 고지문: 마커 안쪽은 자동 관리, 직접 편집 금지
- 기존 ShellIntegrationSection 스타일(settings-row/switch) 매칭

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
워커가 순수 코어만 테스트하고 IO 레이어(apply_harness/symlink/원자쓰기)는
미검증이었던 갭을 닫는다. temp HOME+repo로 실파일 동작 입증:
- 블록 주입 시 마커 밖 사용자 콘텐츠 보존
- 심링크 backup 가드: 사용자 실디렉터리는 .pre-pharos-*로 백업, 삭제 안 함
- build_status 버전/링크 상태 정확
- needs_sync 적용 후 수렴(런치훅 no-op)
- fable 비활성 → 재적용 시 해당 블록만 제거, 나머지·사용자 콘텐츠 유지

harness 테스트 11 → 12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
install.sh 5~6단계는 최초 부트스트랩으로 유지하되, 이후 동기화는 GUI 앱이
담당함을 명시(런치훅 version-gate 재적용 + Settings→Harness 토글). 하네스
업데이트에 install.sh 재실행 불필요 — 앱 재시작이면 됨. CLI-only는 여전히 스크립트 의존.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@dong-park, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 38 minutes and 39 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 55d3e400-3086-4f67-b314-8c530021d8b1

📥 Commits

Reviewing files that changed from the base of the PR and between c3114a2 and 2e555a3.

📒 Files selected for processing (1)
  • src-tauri/src/harness.rs
📝 Walkthrough

Walkthrough

Adds a complete harness synchronization system to the Tauri app. A new harness.rs module implements marker-delimited CLAUDE.md block injection/removal, version tracking, persisted HarnessConfig, filesystem apply with symlink management, drift detection, and Tauri IPC commands. These are wired into lib.rs at startup and surfaced in SettingsPanel.tsx as a new Harness settings section.

Changes

Harness Sync System

Layer / File(s) Summary
Block manipulation and compute primitives
src-tauri/src/harness.rs
Implements inject_block, remove_block, block_version, needs_update, and compute_claude_md for idempotent marker-delimited CLAUDE.md region management.
HarnessConfig persistence and ComponentStatus snapshot
src-tauri/src/harness.rs
Defines HarnessConfig with JSON load/save at ~/.config/pharos/harness.json, path helpers, block/unit component metadata, and ComponentStatus snapshot builder.
Filesystem apply, drift detection, and launch sync
src-tauri/src/harness.rs
Implements backup-before-overwrite helpers, atomic CLAUDE.md writes, symlink creation/removal with backup guard, needs_sync drift detection, and auto_sync_on_launch.
Tauri commands and app startup wiring
src-tauri/src/harness.rs, src-tauri/src/lib.rs
Exposes harness_status, sync_harness, set_component_enabled as Tauri commands; wires module into lib.rs with IPC handler registration and spawn_blocking startup call.
HarnessSection UI in SettingsPanel
src/components/SettingsPanel.tsx
Adds ComponentStatus TypeScript model, version-diff rendering helpers, HarnessSection component with toggle/resync IPC calls, and inserts the section into the Settings layout.
Tests and documentation
src-tauri/src/harness.rs, README.md, install.sh
Unit and IO tests covering primitives, symlink backup-guard, status correctness, and config toggling; README harness sync section; install.sh bootstrap-only clarification comment.

Sequence Diagram(s)

sequenceDiagram
  participant HarnessSection
  participant harness_status
  participant set_component_enabled
  participant sync_harness
  participant apply_harness

  HarnessSection->>harness_status: invoke on mount
  harness_status-->>HarnessSection: Vec~ComponentStatus~

  HarnessSection->>set_component_enabled: toggle component
  set_component_enabled->>apply_harness: update config and apply
  apply_harness-->>set_component_enabled: ok
  set_component_enabled-->>HarnessSection: Vec~ComponentStatus~

  HarnessSection->>sync_harness: manual resync
  sync_harness->>apply_harness: apply harness
  apply_harness-->>sync_harness: ok
  sync_harness-->>HarnessSection: Vec~ComponentStatus~
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 Hop hop, the harness is here to stay,
Markers aligned in CLAUDE.md today!
Symlinks dance from repo to ~/.claude,
Version drift caught — no need to rerun a script raw.
Just restart the app and watch the blocks sync true,
A rabbit-built system, tidy and new! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main feature: app-managed harness synchronization with component toggles, which aligns with the PR's core functionality.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/harness-sync

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
src-tauri/src/harness.rs (2)

96-109: 💤 Low value

Version parsing truncates at hyphen, breaking prerelease versions.

The take_while condition stops at -, which means semver prerelease versions like 1.0.0-beta.1 would be parsed as 1.0.0. If prerelease versions are used in the source blocks, version comparison will be incorrect.

If prerelease versions are not expected, this is fine. Otherwise, stop only at whitespace and let the --> be handled by the subsequent whitespace:

♻️ Fix if prerelease versions are needed
     let v: String = text[start..]
         .chars()
-        .take_while(|c| !c.is_whitespace() && *c != '-')
+        .take_while(|c| !c.is_whitespace())
         .collect();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/harness.rs` around lines 96 - 109, The block_version function
truncates version strings at the hyphen character, which breaks semver
prerelease versions like 1.0.0-beta.1. To fix this, modify the take_while
condition in the block_version function to only check for whitespace characters
and remove the check for the hyphen character. The closing comment marker -->
will be naturally handled by the whitespace condition, so the character check
for *c != '-' should be removed from the logical condition.

539-593: ⚖️ Poor tradeoff

Test mutates global environment variables, risking parallel test interference.

Modifying HOME and PHAROS_DIR affects the entire process. Rust tests run in parallel by default, so concurrent tests reading these env vars could observe incorrect values. While the comment notes "no other test reads those," this is fragile as the codebase grows.

Consider using #[serial] from serial_test crate, or restructuring helpers to accept paths as parameters for testability.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/harness.rs` around lines 539 - 593, The test is mutating global
environment variables HOME and PHAROS_DIR via std::env::set_var, which can cause
interference when tests run in parallel since Rust tests run concurrently by
default. To fix this, add the #[serial] attribute from the serial_test crate to
the test function containing this code (the function with the apply_harness()
call and subsequent assertions). This will ensure the test runs serially and
prevent other tests from observing incorrect environment variable values during
execution. Additionally, make sure the serial_test crate is added to the
dev-dependencies in Cargo.toml if not already present.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/src/harness.rs`:
- Around line 193-195: The home_dir() function returns an empty PathBuf when the
HOME environment variable is unset, which causes subsequent path joins to
produce relative paths and write files to the current working directory instead
of the user's home directory. Replace the current implementation that uses
std::env::var("HOME").map(PathBuf::from).unwrap_or_default() with the dirs
crate's home_dir() function, which provides platform-specific fallbacks and
handles the HOME variable more robustly. If the dirs crate is not available, add
it as a dependency first.
- Around line 274-280: The is_our_symlink function compares symlink targets
directly without normalizing path representations, causing failures when
install.sh stores a relative symlink target (like ./pharos/claude/skills) while
harness.rs compares against an absolute path. Modify the comparison logic to
canonicalize both the symlink target returned by read_link and the src path
using a path normalization approach (either canonical paths or consistent
relative/absolute resolution) before comparing them with the equality check,
ensuring the comparison succeeds regardless of whether paths are represented as
relative or absolute.

In `@src-tauri/src/lib.rs`:
- Around line 242-245: The harness operations are susceptible to race conditions
because auto_sync_on_launch, sync_harness, and set_component_enabled all run
concurrently via spawn_blocking and can interleave access to shared config and
symlink state. Add a static Mutex at the module level to serialize all harness
operations. Wrap the auto_sync_on_launch function body with a lock acquisition,
and similarly protect the apply_harness() calls and read-modify-write sequences
in set_component_enabled to ensure only one harness operation executes at a
time. This prevents lost updates to component toggles and symlink conflicts.

---

Nitpick comments:
In `@src-tauri/src/harness.rs`:
- Around line 96-109: The block_version function truncates version strings at
the hyphen character, which breaks semver prerelease versions like 1.0.0-beta.1.
To fix this, modify the take_while condition in the block_version function to
only check for whitespace characters and remove the check for the hyphen
character. The closing comment marker --> will be naturally handled by the
whitespace condition, so the character check for *c != '-' should be removed
from the logical condition.
- Around line 539-593: The test is mutating global environment variables HOME
and PHAROS_DIR via std::env::set_var, which can cause interference when tests
run in parallel since Rust tests run concurrently by default. To fix this, add
the #[serial] attribute from the serial_test crate to the test function
containing this code (the function with the apply_harness() call and subsequent
assertions). This will ensure the test runs serially and prevent other tests
from observing incorrect environment variable values during execution.
Additionally, make sure the serial_test crate is added to the dev-dependencies
in Cargo.toml if not already present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 06b319fd-bc41-4663-8fe0-db980bd89ec7

📥 Commits

Reviewing files that changed from the base of the PR and between 2cdace1 and c3114a2.

📒 Files selected for processing (5)
  • README.md
  • install.sh
  • src-tauri/src/harness.rs
  • src-tauri/src/lib.rs
  • src/components/SettingsPanel.tsx

Comment thread src-tauri/src/harness.rs
Comment on lines +193 to +195
fn home_dir() -> PathBuf {
std::env::var("HOME").map(PathBuf::from).unwrap_or_default()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

home_dir() returns empty path when HOME is unset, causing relative path writes.

If HOME is unset (rare but possible), PathBuf::default() returns an empty path. Subsequent joins produce relative paths like .claude/CLAUDE.md, potentially writing harness content into the current working directory rather than the user's home.

Consider returning Option<PathBuf> or using dirs::home_dir() crate which handles platform-specific fallbacks.

🛡️ Defensive fallback suggestion
 fn home_dir() -> PathBuf {
-    std::env::var("HOME").map(PathBuf::from).unwrap_or_default()
+    std::env::var("HOME")
+        .map(PathBuf::from)
+        .unwrap_or_else(|_| {
+            // Fallback to /tmp to avoid writing to CWD; auto_sync_on_launch
+            // will skip anyway since repo won't exist there.
+            eprintln!("harness: HOME 환경변수가 없어 기본 경로 사용 불가");
+            PathBuf::from("/tmp/pharos-no-home")
+        })
 }
📝 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
fn home_dir() -> PathBuf {
std::env::var("HOME").map(PathBuf::from).unwrap_or_default()
}
fn home_dir() -> PathBuf {
std::env::var("HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
// Fallback to /tmp to avoid writing to CWD; auto_sync_on_launch
// will skip anyway since repo won't exist there.
eprintln!("harness: HOME 환경변수가 없어 기본 경로 사용 불가");
PathBuf::from("/tmp/pharos-no-home")
})
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/harness.rs` around lines 193 - 195, The home_dir() function
returns an empty PathBuf when the HOME environment variable is unset, which
causes subsequent path joins to produce relative paths and write files to the
current working directory instead of the user's home directory. Replace the
current implementation that uses
std::env::var("HOME").map(PathBuf::from).unwrap_or_default() with the dirs
crate's home_dir() function, which provides platform-specific fallbacks and
handles the HOME variable more robustly. If the dirs crate is not available, add
it as a dependency first.

Comment thread src-tauri/src/harness.rs
Comment on lines +274 to +280
/// True when `dst` is a symlink that points at exactly `src` (i.e. ours).
fn is_our_symlink(dst: &Path, src: &Path) -> bool {
std::fs::symlink_metadata(dst)
.map(|m| m.file_type().is_symlink())
.unwrap_or(false)
&& std::fs::read_link(dst).map(|c| c == *src).unwrap_or(false)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how install.sh creates symlinks to understand path representation
rg -n 'ln -s' --type=sh -A2 -B2

Repository: dong-park/pharos

Length of output: 194


🏁 Script executed:

# Find where is_our_symlink is called and what src parameter is passed
rg -n 'is_our_symlink' src-tauri/src/harness.rs -B3 -A3

Repository: dong-park/pharos

Length of output: 1415


🏁 Script executed:

# Check install.sh to see how it computes the src path for symlinks
rg -n 'src=' install.sh -B5 -A5 | head -100

Repository: dong-park/pharos

Length of output: 313


🏁 Script executed:

# Look at the broader context of symlink creation in harness.rs (around line 353 mentioned in scratchpad)
sed -n '345,365p' src-tauri/src/harness.rs

Repository: dong-park/pharos

Length of output: 785


🏁 Script executed:

# Find where apply_unit and remove_unit are called to see src parameter sources
rg -n 'apply_unit|remove_unit' src-tauri/src/harness.rs -B2 -A2

Repository: dong-park/pharos

Length of output: 525


🏁 Script executed:

# Find the repo_dir() function to understand how base path is computed
rg -n 'fn repo_dir|repo_dir()' src-tauri/src/harness.rs -B2 -A5

Repository: dong-park/pharos

Length of output: 1424


🏁 Script executed:

# Check install.sh to see how it determines the repo path
rg -n 'REPO|repo' install.sh | head -20

Repository: dong-park/pharos

Length of output: 403


🏁 Script executed:

# Get the complete link() function from install.sh
sed -n '246,280p' install.sh

Repository: dong-park/pharos

Length of output: 1046


🏁 Script executed:

# Also check if there's any path normalization or expansion happening in install.sh
rg -n 'realpath|canonicalize|readlink|ln -s' install.sh -B3 -A3

Repository: dong-park/pharos

Length of output: 383


Symlink target comparison may fail if paths differ in representation between install.sh and harness.rs.

read_link returns the raw symlink target as stored. If install.sh is run with PHAROS_DIR as a relative path (e.g., ./pharos) or an absolute path different from harness.rs's computed default, the equality check c == *src will fail even when both point to the same directory.

Example: If install.sh stores a symlink target as ./pharos/claude/skills but harness.rs compares against /home/user/pharos/claude/skills (absolute), the ownership check fails incorrectly.

Consider canonicalizing both paths before comparison, or ensure both use identical path resolution logic (same PHAROS_DIR env var or default).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/harness.rs` around lines 274 - 280, The is_our_symlink function
compares symlink targets directly without normalizing path representations,
causing failures when install.sh stores a relative symlink target (like
./pharos/claude/skills) while harness.rs compares against an absolute path.
Modify the comparison logic to canonicalize both the symlink target returned by
read_link and the src path using a path normalization approach (either canonical
paths or consistent relative/absolute resolution) before comparing them with the
equality check, ensuring the comparison succeeds regardless of whether paths are
represented as relative or absolute.

Comment thread src-tauri/src/lib.rs
Comment on lines +242 to +245
// Keep ~/.claude aligned with the repo's harness source blocks +
// unit symlinks. Off the UI thread, never blocks startup: re-applies
// only on drift, silently skips when the repo isn't present.
let _ = tauri::async_runtime::spawn_blocking(harness::auto_sync_on_launch);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Concurrent access to harness files creates race conditions.

The fire-and-forget auto_sync_on_launch can run concurrently with user-triggered sync_harness or set_component_enabled commands, since all use spawn_blocking. This creates multiple race conditions:

  1. Config lost-update: set_component_enabled uses read-modify-write (load → modify → save). If two calls overlap, the second save will overwrite the first, losing component toggle changes.

  2. Concurrent apply_harness() calls: All three code paths call apply_harness(), which writes CLAUDE.md and manages symlinks. Concurrent execution can cause interleaved writes or symlink conflicts.

The race window is small (startup task completes quickly and frontend calls are user-triggered), but the data corruption risk is real.

🔒 Add synchronization around harness operations

Add a static mutex to serialize harness operations:

// At top of harness.rs or lib.rs
use std::sync::Mutex;
use once_cell::sync::Lazy;

static HARNESS_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));

Then wrap harness operations:

pub fn auto_sync_on_launch() {
    let _guard = HARNESS_LOCK.lock().unwrap();
    // ... existing implementation
}

// Similarly for apply_harness() and config operations in set_component_enabled

Alternatively, use filesystem-level file locking (e.g., flock on Unix) around config read-modify-write sequences.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/lib.rs` around lines 242 - 245, The harness operations are
susceptible to race conditions because auto_sync_on_launch, sync_harness, and
set_component_enabled all run concurrently via spawn_blocking and can interleave
access to shared config and symlink state. Add a static Mutex at the module
level to serialize all harness operations. Wrap the auto_sync_on_launch function
body with a lock acquisition, and similarly protect the apply_harness() calls
and read-modify-write sequences in set_component_enabled to ensure only one
harness operation executes at a time. This prevents lost updates to component
toggles and symlink conflicts.

std::os::unix::fs::symlink은 Windows에 없어 GUI installer 빌드가
E0433로 실패. cfg(unix/windows)로 symlink_impl 분리, Windows는
symlink_dir/symlink_file 사용.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dong-park dong-park merged commit e25abec into main Jun 17, 2026
5 checks passed
@dong-park dong-park deleted the feat/harness-sync branch June 17, 2026 07:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant