Back to blog
FILE 0xFC·WORKSPACE PERMISSION DENIED WHEN SCP'ING FROM AUTOMATION RUN

Workspace permission denied when SCP'ing from automation runner

May 6, 2026 · automation, ssh, linux

I have a small homelab automation runner that orchestrates jobs across a handful of remote machines. New job, new bug: SCP transfers from the runner to a target box were failing with permission denied — but only on the local workspace side, before the file even left the runner.

What was happening

The runner creates a per-job workspace under /opt/runner/workspace/<job-uuid>/. It does some prep work there, then SCPs the staged files to the target. The SCP step failed:

Permission denied
scp: failed to write '/opt/runner/workspace/<uuid>/temp/payload.zip': Permission denied
lost connection

The interesting detail: the path was on the runner, not the remote. The runner was failing to write a temp file inside its own workspace.

What I found

Earlier in the job's lifecycle, a download step had run as a different user (via sudo) and dropped a file into the workspace. That file ended up owned by root, and the workspace temp/ subdirectory it lived in inherited root-only permissions because the download step had created the subdirectory itself.

When the SCP step ran later as the runner's own service user, it couldn't write into the now-root-owned subdirectory. The error message is technically correct but misleading — it says "Permission denied" against a path you'd expect the service to own, which makes you go looking on the remote side first.

The fix wasn't to escalate SCP; the fix was to make sure no earlier step left a stranger owning files in the workspace.

The fix

Two layers. First, the immediate cleanup:

chown -R runner:runner /opt/runner/workspace/<uuid>/

Second, the runner now wraps any step that needs sudo (or any privileged context) with a post-step chown back to the runner user:

def run_step(step):
    workspace = step.workspace_dir
    try:
        step.execute()
    finally:
        # any privileged step might leave stranger-owned files behind;
        # always claim them back so later steps can read/write.
        subprocess.run(
            ["chown", "-R", f"{RUNNER_USER}:{RUNNER_USER}", workspace],
            check=False,
        )

check=False because chown failures aren't fatal — if it's already correct, we move on.

What I'd do differently

In any multi-step pipeline that mixes privileged and unprivileged steps in a shared workspace, ownership drift is going to happen sooner or later. The robust pattern is to either (a) give the workspace a sticky bit and a group everyone can write through, or (b) reset ownership after each step. I went with (b) because it's explicit — you can see in the logs that ownership got normalized, and any future weirdness shows up clearly.

The misleading error message taught me a thing too. When SCP fails with a path on one side, double-check that the path is actually on the side you think it's on. Read the error twice before SSHing into the wrong machine.