Skip to content

Commit 2009cd4

Browse files
RealZSTclaude
andcommitted
fix: allow file access within registered project paths in web mode
Previously, web API handlers restricted file access to $HOME only, blocking legitimate workspace paths on servers (e.g. /scratch/work/). Now paths within registered project directories are also allowed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a9b4272 commit 2009cd4

4 files changed

Lines changed: 35 additions & 12 deletions

File tree

crates/hk-web/src/handlers/agents.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ use crate::state::WebState;
1010
type Result<T> = std::result::Result<Json<T>, ApiError>;
1111

1212
/// Resolve `~` and validate custom config paths (mirrors desktop logic).
13-
fn resolve_and_validate_config_path(path: &str) -> std::result::Result<String, hk_core::HkError> {
13+
fn resolve_and_validate_config_path(
14+
path: &str,
15+
store: &hk_core::store::Store,
16+
) -> std::result::Result<String, hk_core::HkError> {
1417
let resolved = if path.starts_with("~/") {
1518
dirs::home_dir()
1619
.map(|h| h.join(&path[2..]).to_string_lossy().to_string())
@@ -24,13 +27,12 @@ fn resolve_and_validate_config_path(path: &str) -> std::result::Result<String, h
2427
));
2528
}
2629
let resolved_path = std::path::Path::new(&resolved);
27-
let home = dirs::home_dir()
28-
.ok_or_else(|| hk_core::HkError::Internal("Cannot determine home directory".into()))?;
29-
if !resolved_path.starts_with(&home) {
30+
if !super::is_path_allowed(resolved_path, store) {
3031
return Err(hk_core::HkError::PathNotAllowed(
31-
"Custom config paths must be within your home directory".into(),
32+
"Custom config paths must be within your home directory or a registered project".into(),
3233
));
3334
}
35+
let home = dirs::home_dir().unwrap_or_default();
3436
if resolved_path == home {
3537
return Err(hk_core::HkError::Validation(
3638
"Cannot use home directory itself as a config path".into(),
@@ -224,8 +226,8 @@ pub async fn add_custom_config_path(
224226
Json(params): Json<AddCustomConfigPathParams>,
225227
) -> Result<i64> {
226228
blocking(move || {
227-
let resolved = resolve_and_validate_config_path(&params.path)?;
228229
let store = state.store.lock();
230+
let resolved = resolve_and_validate_config_path(&params.path, &store)?;
229231
Ok(store.add_custom_config_path(&params.agent, &resolved, &params.label, &params.category)?)
230232
}).await
231233
}
@@ -243,8 +245,8 @@ pub async fn update_custom_config_path(
243245
Json(params): Json<UpdateCustomConfigPathParams>,
244246
) -> Result<()> {
245247
blocking(move || {
246-
let resolved = resolve_and_validate_config_path(&params.path)?;
247248
let store = state.store.lock();
249+
let resolved = resolve_and_validate_config_path(&params.path, &store)?;
248250
store.update_custom_config_path(params.id, &resolved, &params.label, &params.category)?;
249251
Ok(())
250252
}).await

crates/hk-web/src/handlers/extensions.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -491,8 +491,8 @@ pub async fn list_skill_files(
491491
|| canonical.starts_with(a.base_dir())
492492
});
493493
if !allowed {
494-
let home = dirs::home_dir().unwrap_or_default();
495-
if !canonical.starts_with(&home) {
494+
let store = state.store.lock();
495+
if !super::is_path_allowed(&canonical, &store) {
496496
return Err(hk_core::HkError::PathNotAllowed(
497497
"Path is not within a known agent directory".into(),
498498
));

crates/hk-web/src/handlers/mod.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,24 @@ pub mod install;
55
pub mod marketplace;
66
pub mod projects;
77
pub mod settings;
8+
9+
use hk_core::store::Store;
10+
use std::path::Path;
11+
12+
/// Check if a canonical path is within the home directory or any registered project path.
13+
pub(crate) fn is_path_allowed(canonical: &Path, store: &Store) -> bool {
14+
if let Some(home) = dirs::home_dir() {
15+
if canonical.starts_with(&home) {
16+
return true;
17+
}
18+
}
19+
if let Ok(projects) = store.list_projects() {
20+
for p in &projects {
21+
let proj_path = Path::new(&p.path);
22+
if canonical.starts_with(proj_path) {
23+
return true;
24+
}
25+
}
26+
}
27+
false
28+
}

crates/hk-web/src/handlers/settings.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,18 +150,18 @@ pub struct ReadConfigPreviewParams {
150150
}
151151

152152
pub async fn read_config_file_preview(
153-
State(_state): State<WebState>,
153+
State(state): State<WebState>,
154154
Json(params): Json<ReadConfigPreviewParams>,
155155
) -> Result<String> {
156156
blocking(move || {
157-
let home = dirs::home_dir().unwrap_or_default();
158157
let file_path = std::path::Path::new(&params.path);
159158
if !file_path.exists() {
160159
return Err(hk_core::HkError::NotFound("File not found".into()));
161160
}
162161
let canonical = std::fs::canonicalize(file_path)
163162
.map_err(|_| hk_core::HkError::NotFound("File not found".into()))?;
164-
if !canonical.starts_with(&home) {
163+
let store = state.store.lock();
164+
if !super::is_path_allowed(&canonical, &store) {
165165
return Err(hk_core::HkError::PermissionDenied("Path not allowed".into()));
166166
}
167167

0 commit comments

Comments
 (0)