Repository: harbor-framework/harbor
Issue: Claude Code agent: Session directory permissions prevent trajectory/metrics extraction
Root cause: Validation Gap

# Claude Code agent: Session directory permissions prevent trajectory/metrics extraction

## Summary

The Claude Code agent creates session directories with restrictive **700 permissions** (owned by root), preventing Harbor from reading trajectory data. This results in:
- No `trajectory.json` file generated
- All token metrics (`n_input_tokens`, `n_output_tokens`, `n_cache_tokens`) remain `null`
- Warning message: "No Claude Code session directory found"

## Root Cause

In `src/harbor/agents/installed/claude_code.py`, the `populate_context_post_run()` method uses `_get_session_dir()` to locate session files:

```python
def _get_session_dir(self) -> Path | None:
    project_root = self.logs_dir / "sessions" / "projects"
    if not project_root.exists():
        return None
    
    candidate_files = list(project_root.glob("**/*.jsonl"))  # ❌ Fails here
    if not candidate_files:
        return None
```

Claude Code creates directories with **700 permissions**:
```
drwx------ 2 root root  sessions/projects/-workspace   (700)
```

Python's `glob()` cannot traverse directories without read/execute permissions, so it returns an empty list even though `.jsonl` files exist inside.

## Impact

- **Metrics unavailable**: Token usage and cost tracking fail
- **No trajectory**: Cannot analyze agent behavior or debug issues
- **Inconsistent behavior**: Other agents (codex, openhands, terminus-2) work correctly

## Reproduction

### Prerequisites
```bash
# Install Harbor
cd /path/to/harbor
uv sync --all-extras

# Set API key
export ANTHROPIC_API_KEY=your_key_here
```

### Create a minimal test task

```bash
mkdir -p /tmp/test-task/{environment,tests}

# Create task.toml
cat > /tmp/test-task/task.toml << 'TOML'
[task]
name = "test-task"
timeout_sec = 300
cpus = 1
memory_mb = 2048
TOML

# Create instruction
cat > /tmp/test-task/instruction.md << 'INST'
# Simple Test Task

Create a file at /workspace/test.txt with the text "Hello Harbor" and print it.
INST

# Create Dockerfile
cat > /tmp/test-task/environment/Dockerfile << 'DOCKER'
FROM python:3.11-slim
WORKDIR /workspace
DOCKER

# Create test script
cat > /tmp/test-task/tests/test.sh << 'TEST'
#!/bin/bash
if [ -f /workspace/test.txt ]; then
    echo '{"reward": 1.0}' > /logs/verifier/reward.json
else
    echo '{"reward": 0.0}' > /logs/verifier/reward.json
fi
TEST
chmod +x /tmp/test-task/tests/test.sh
```

### Run Harbor with Claude Code

```bash
uv run harbor run \
  --path /tmp/test-task \
  --agent claude-code \
  --model anthropic/claude-sonnet-4-5 \
  --jobs-dir /tmp/test-results \
  --n-concurrent 1
```

### Verify the Bug

```bash
# Find the trial directory
TRIAL_DIR=$(find /tmp/test-results -name "test-task__*" -type d | head -1)

# Check permissions (you'll see 700)
sudo ls -la "$TRIAL_DIR/agent/sessions/projects/"

# Verify metrics are null
cat "$TRIAL_DIR/result.json" | jq '.agent_result'
# Output:
# {
#   "n_input_tokens": null,
#   "n_cache_tokens": null,
#   "n_output_tokens": null,
#   "cost_usd": null
# }

# Verify trajectory.json is missing
ls "$TRIAL_DIR/agent/trajectory.json"
# ls: cannot access: No such file or directory

# BUT the data exists (requires sudo to access)
sudo find "$TRIAL_DIR/agent/sessions" -name "*.jsonl"
# Shows: .../sessions/projects/-workspace/578b2386-....jsonl

# Verify it contains usage data
sudo cat "$TRIAL_DIR/agent/sessions/projects/-workspace/"*.jsonl | grep -o '"usage":{[^}]*}' | head -1
# Shows: "usage":{"input_tokens":1,"cache_read_input_tokens":16333,"output_tokens":1}
```

## Comparison: Codex Agent (Works Correctly)

The codex agent creates directories with **755 permissions**:
```
drwxr-xr-x 3 root root  sessions/2025/11/29   (755)
```

Harbor can read these files, resulting in:
```json
{
  "n_input_tokens": 10261,
  "n_cache_tokens": 3328,
  "n_output_tokens": 99
}
```

## Proposed Solution

### Option 1: Fix permissions in Harbor
Modify `_get_session_dir()` to handle permission errors gracefully by using `sudo` or changing ownership:

```python
def _get_session_dir(self) -> Path | None:
    project_root = self.logs_dir / "sessions" / "projects"
    if not project_root.exists():
        return None
    
    # Fix permissions before glob
    try:
        subprocess.run(
            ["chmod", "-R", "a+rX", str(project_root)],
            check=True,
            capture_output=True
        )
    except subprocess.CalledProcessError:
        self.logger.warning(f"Failed to fix permissions on {project_root}")
        return None
    
    candidate_files = list(project_root.glob("**/*.jsonl"))
    ...
```

### Option 2: Fix in Claude Code (upstream)
Request Claude Code CLI to create session directories with 755 permissions instead of 700.

### Option 3: Workaround via docker-compose
Add a background permission-fixing loop (mentioned in the original reproduction docs):

```yaml
command:
  - sh
  - -c
  - |
    (while true; do sleep 10; chmod -R a+rX /logs/agent/sessions 2>/dev/null || true; done &)
    sleep infinity
```

## Environment
- Harbor version: latest (commit 8c737de)
- Claude Code CLI: 2.0.55
- Python: 3.13.7
- Docker environment

## Related Files
- `src/harbor/agents/installed/claude_code.py:686-704` (populate_context_post_run)
- `src/harbor/agents/installed/claude_code.py:464-478` (_get_session_dir)


@alexgshaw  Could you have a look? 

I started testing harbor and really love it. The issue happened when I tried to understand all the features including ATIF. In my separate project, I have used "Option 3: Workaround via docker-compose" as a temporary, local workaround.

I could submit a fix based on option 1, if you think that works well.



I couldn't reproduce this bug. Can you please share the exact command you use? Please use latest main branch and use "uv run harbor ..."

 ## Environment
  - Harbor version: Latest main branch
  - Python: 3.13.7

## Exact Command Used
Note: I test it with CLAUDE_CODE_OAUTH_TOKEN  (newly merged at https://github.com/laude-institute/harbor/pull/173), because I don't have ANTHROPIC_API_KEY.

```bash
$ uv run harbor run -a claude-code -p examples/tasks/hello-world -o tmp/jobs
Failed to convert Claude Code events to trajectory: [Errno 13] Permission denied: 
'tmp/jobs/2025-11-29__21-28-21/hello-world__vE7bSjV/agent/sessions/projects/-app/agent-b69a87fc.jsonl'
Failed to convert Claude Code events to trajectory: [Errno 13] Permission denied: 
'tmp/jobs/2025-11-29__21-28-21/hello-world__vE7bSjV/agent/sessions/projects/-app/agent-b69a87fc.jsonl'
  1/1 Mean: 1.000 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0:00:36 0:00:00
Results written to tmp/jobs/2025-11-29__21-28-21/result.json
        claude-code on adhoc         
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓
┃ Metric              ┃ Value       ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩
│ Agent               │ claude-code │
│ Dataset             │ adhoc       │
│ Trials              │ 1           │
│ Errors              │ 0           │
│                     │             │
│ Mean                │ 1.000       │
│                     │             │
│ Reward Distribution │             │
│   reward = 1.0      │ 1           │
└─────────────────────┴─────────────┘
```

## Result folder structure

```
  tmp/jobs/2025-11-29__21-28-21/
    hello-world__vE7bSjV/
      agent/
        sessions/
        command-0/
        command-1/
        setup/
        claude-code.txt
        install.sh
      config.json
      verifier/
        test-stdout.txt
        ctrf.json
        reward.txt
      trial.log
      result.json
    config.json
    job.log
    result.json
```

The files inside the subfolder of `sessions` is not accessible on the host
```
drwxr-xr-x 8 root     root     4096 Nov 29 21:28 .
-rw------- 1 root     root      396 Nov 29 21:28 .claude.json
-rw------- 1 root     root      367 Nov 29 21:28 .claude.json.backup
```



Hmmm, I still cannot reproduce it. Were you using sudo/root account?

I tested to run it on the Macbook, and there is no such issue. Not sure what caused the difference.
```
$ uv run harbor run -a claude-code -p examples/tasks/hello-world -o tmp/jobs
Wrote Claude Code trajectory to tmp/jobs/2025-11-29__22-54-42/hello-world__AcSdWho/agent/trajectory.json
  1/1 Mean: 1.000 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0:01:28 0:00:00
Results written to tmp/jobs/2025-11-29__22-54-42/result.json
        claude-code on adhoc         
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓
┃ Metric              ┃ Value       ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩
│ Agent               │ claude-code │
│ Dataset             │ adhoc       │
│ Trials              │ 1           │
│ Errors              │ 0           │
│                     │             │
│ Mean                │ 1.000       │
│                     │             │
│ Reward Distribution │             │
│   reward = 1.0      │ 1           │
```

I observed the previous issue when running on Ubuntu 24.04.2 with a regular user account (without sudo). I just tested it on a different Linux machine with Ubuntu 24.04.2 and I can easily reproduce the issue.

Could have a try on Linux machine too? @li-boxuan 


Interesting, I indeed used a macbook. Let me try this on a Linux machine.

Thank you, I was able to reproduce it on a Linux machine

@ai-jz I think option 1 sounds good!



This should be fixed in [557](https://github.com/laude-institute/harbor/pull/557). 

This is fixed now for docker 

15eb03c39c4757ebe3617bfbf2f4da6b8c649fc9

I'm still hitting this in Harbor 0.3.0. Could someone please take a look at #1024