Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

Git deployment failures that say “permission denied” have two fundamentally different causes. Permission denied (publickey) means the VPS could not authenticate to the Git server over SSH. A local message such as cannot open .git/FETCH_HEAD: Permission denied means the account running Git cannot traverse, read, or write the VPS filesystem. Identify the layer first; changing Linux modes will not repair a rejected SSH key, and replacing SSH keys will not make a root-owned checkout writable by your deploy user.

This guide shows how to identify the failing identity, correct SSH and repository permissions without running Git as root, repair push-to-deploy hooks, and diagnose CI or rsync transfers.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Classify the error before changing permissions

Capture the complete error from the exact command that fails. The wording usually identifies the layer:

Message or symptom Likely layer First check
Permission denied (publickey) Remote SSH authentication or authorization Test the same host as the git user with verbose SSH output and inspect which key is offered.
fatal: cannot open .git/FETCH_HEAD: Permission denied, unable to create an index, or checkout files Local VPS ownership, mode bits, or directory traversal Check the account running Git, the checkout and .git ownership, and every parent directory.
Manual pull succeeds, but a hook, cron job, or CI SSH command fails Different runtime identity or environment Log id, pwd, PATH, HOME, and the command’s destination as that process.
rsync: Permission denied (13) Remote destination or metadata operation Verify the destination owner and test whether timestamp or other metadata updates are allowed.

Do not repeatedly run chmod -R 777 or switch to root. Broad modes can expose source code and secrets, while root-created files often cause the next deployment to fail.

#1 Best Overall

Record the failing identity and configuration

Run these commands in the checkout (or in the hook’s working directory) before making changes:

id
pwd
git remote -v
printf 'HOME=%snPATH=%sn' "$HOME" "$PATH"
stat -c '%A %U:%G %n' . .git
namei -l "$(pwd)"

id tells you the numeric and named user and groups. git remote -v shows whether the remote is an SSH URL or HTTPS URL. stat reports the checkout and Git metadata ownership; namei -l displays permissions on every parent component, which is important because a user needs execute (traverse) permission on each directory, not just write permission on the final one.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For a service, hook, cron entry, or CI runner, put equivalent output in a temporary log. A command that works in your interactive shell may be running as a different account with a different home directory, SSH agent, known-hosts file, and PATH.

Fix remote SSH authentication failures

Confirm the remote host and required account

Git hosting services normally expect the SSH login name git, even when the key belongs to a person, deploy key, or application. From the deployment checkout, run:

git remote -v
ssh -vT git@github.com

Use the equivalent host for another Git provider. The verbose trace shows the host, SSH configuration files, identities considered, and whether the server accepted a key. A server rejection is an authentication or authorization problem, not a local repository mode problem.

Test the exact key used by deployment

If the deployment uses a non-default key, test it explicitly:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
ssh -i ~/.ssh/deploy_app_ed25519 -vT git@github.com

Replace the path with the private key that the deployment account can read. If that succeeds while an unqualified test fails, make the selection permanent for that account with an SSH configuration entry:

Host github.com
User git
IdentityFile ~/.ssh/deploy_app_ed25519
IdentitiesOnly yes

Protect the private key and its directory. The deployment user must own the key (or have deliberate group access), and the key must not be readable by unrelated users. Check the public half that you attach to the host or repository:

ssh-keygen -y -f ~/.ssh/deploy_app_ed25519

Compare the output with the public key registered for the intended account or repository. A key can be perfectly valid yet rejected because it is attached to the wrong account or repository.

Check deploy-key scope

A deploy key is tied to one repository. It is read-only by default; write access must be explicitly granted. Deploy keys do not expire automatically, so rotate them when staff, hosts, or operating procedures change. A key that works for one repository can still be rejected for another. For deployments spanning many repositories, a Git hosting application with narrowly assigned repository permissions is easier to manage than copying one private key everywhere.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Do not mix root and normal-user SSH environments

Git hosting documentation advises against using sudo or administrator privileges with Git. sudo git pull changes the effective user to root, which normally means:

  • SSH reads /root/.ssh instead of the deploy user’s key and configuration.
  • A different SSH agent and known-hosts file are used.
  • Files written by the command become owned by root.
  • Repository-level Git configuration and credential helpers may differ.

Run the deployment as a dedicated non-root account. If one controlled privileged action is unavoidable (for example, restarting a service), keep Git and the checkout under the deploy account and make the privileged command explicit in a narrowly scoped service policy. Verify the environment rather than assuming it matches your login shell.

Repair local VPS ownership and write access

Inspect the checkout, metadata, hooks, and parents

Check all paths that the failing operation touches:

id deploy
stat -c '%A %U:%G %n' /srv/www/app /srv/www/app/.git /srv/www/app/.git/FETCH_HEAD
namei -l /srv/www/app/.git/FETCH_HEAD
find /srv/www/app ( ! -user deploy -o ! -group deploy ) -ls | head -n 50

Substitute the actual deploy account and project path. A pull may need to write .git/FETCH_HEAD, pack files, refs, logs, and the working tree. A hook may additionally need to execute a script, create an application log, write a release directory, or restart a process. Inspect those destinations instead of assuming the checkout is the only path.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Give the deploy account ownership of what it updates

After confirming the intended account, correct ownership narrowly:

sudo chown -R deploy:deploy /srv/www/app

Use a different group if your operating model requires one. Do not run this blindly on a shared directory or a live tree containing files intentionally owned by another service. Review the result:

stat -c '%A %U:%G %n' /srv/www/app /srv/www/app/.git
find /srv/www/app -maxdepth 2 -printf '%u:%g %m %pn' | sort | head -n 50

Set restrictive, functional modes rather than world-writable modes. A common baseline is owner read/write and group read/write where collaboration is required, with directory execute permission for traversal. Keep private configuration and keys more restrictive than source files.

Check parent-directory traversal and mounts

A user can be blocked before reaching the checkout if a parent such as /srv or /srv/www lacks execute permission for that user or group. namei -l reveals this. Also verify that the path is mounted read-only or subject to an access-control policy:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
findmnt -T /srv/www/app
mount | grep ' /srv/www '

If the filesystem is read-only, fix the mount or storage problem instead of changing file modes. Mandatory access-control systems, container bind mounts, and network filesystems can impose restrictions that ordinary Unix mode bits do not show; inspect the service and kernel logs when ownership appears correct but writes are still denied.

Keep web-server access separate from Git write access

If the web server only serves the deployed files, it usually needs read and directory-traverse access, not ownership of the repository or its .git directory. Grant read access through a shared group or carefully chosen modes, and keep deployment writes with the dedicated deploy account. Never expose .git to HTTP; configure the web root or server rules so repository metadata and private files cannot be downloaded.

Make push-to-deploy hooks reliable

A common design is a bare repository receiving an SSH push and a post-receive hook checking out a branch into a working tree. The hook must be executable and must run as the account that owns the destination.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Install and permission the hook

  1. Create or use a bare repository owned by the deploy account, such as /srv/git/app.git.
  2. Place the hook at /srv/git/app.git/hooks/post-receive.
  3. Make it executable: chmod 750 /srv/git/app.git/hooks/post-receive.
  4. Ensure the hook’s interpreter, checkout directory, log path, build tools, and restart command are accessible to that account.

A deterministic hook sets absolute paths, a safe PATH, the branch it accepts, and a log destination. For example:

#!/bin/sh
set -eu
PATH=/usr/local/bin:/usr/bin:/bin
export PATH
GIT_DIR=/srv/git/app.git
WORK_TREE=/srv/www/app
LOG=/var/log/app-deploy.log

while read oldrev newrev refname; do
[ "$refname" = "refs/heads/main" ] || continue
printf '%s deploying %s to %sn' "$(date -Is)" "$newrev" "$WORK_TREE" >> "$LOG"
git --work-tree="$WORK_TREE" --git-dir="$GIT_DIR" checkout -f main >> "$LOG" 2>&1
printf '%s deployed %sn' "$(date -Is)" "$newrev" >> "$LOG"
done

The example assumes the deploy account can append to the log and write the work tree. If those paths are root-owned, the hook will fail even though the incoming SSH push was authenticated. Avoid unqualified commands and interactive assumptions; hooks run with a minimal environment.

Verify the result and diagnose hook failures

After a test push, verify both the checked-out commit and the hook log:

git -C /srv/www/app rev-parse HEAD
tail -n 50 /var/log/app-deploy.log
stat -c '%A %U:%G %n' /srv/www/app

If the commit is correct but the application is stale, the failure is after checkout—usually a build, cache, or restart command. If no log entry appears, check hook execute permission, the bare repository path, and the SSH account receiving the push.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Fix CI and rsync deployments

Match the private key to the VPS account

For GitHub Actions or another CI runner, store the private key in the runner’s secret configuration and place its matching public key in the VPS user’s ~/.ssh/authorized_keys. The CI command must connect to the same username and host you tested manually. Confirm the VPS account’s home-directory, .ssh, and authorized_keys ownership and modes, and ensure the account’s login shell permits the intended command.

Check the remote destination and metadata operations

With rsync, the destination directory must be writable by the remote account. A transfer can also fail when the files themselves are writable but the filesystem rejects timestamp or ownership changes. Test a minimal transfer to a disposable directory, then review options that request metadata the account cannot set. When timestamp preservation is the only failing operation and your deployment does not require it, an option such as --no-times can avoid that metadata write; use it deliberately rather than hiding a broader ownership problem.

Log the complete CI command (excluding secrets), remote username, destination, and exit status. Reproduce over SSH as the same account where possible; an interactive test as your personal user does not validate the CI path.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Choose a deployment design that limits permission failures

Design consideration Single-repository deploy key Application or multi-repository identity Dedicated VPS deploy user Staged releases with a switch
Credential scope One repository Several explicitly granted repositories Controls local filesystem access Same credential model, separate release paths
Least privilege Read-only by default; write must be enabled Fine-grained repository permissions when configured correctly Limit ownership and sudo rules to deployment tasks Deploy user writes releases and a controlled pointer change
Rotation and expiry Does not expire automatically; rotate manually Managed credential lifecycle depends on the application Rotate SSH keys and remove old authorized keys Retain old releases for rollback and remove them with policy
Operational visibility SSH and Git logs Provider audit events plus deployment logs Hook, service, and shell logs Release IDs, health checks, and switch logs
Filesystem risk Checkout must be writable by one account Same, with more repositories to audit Separates deployment writes from web serving A failed release need not overwrite the currently served release

For a small installation, a dedicated non-root deploy user, repository-scoped credentials, an executable hook or CI job, and explicit logs are usually sufficient. As the number of repositories and operators grows, use an identity with narrowly assigned repository access and a staged-release layout so rollback does not require rewriting a live tree.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use these recovery branches for common symptoms

The SSH test succeeds, but git pull fails locally

The remote authentication is working. Inspect local ownership, .git files, parent traversal, and mount state as the account that runs the pull. Correct the checkout and metadata ownership; do not add a second SSH key.

The pull works interactively but fails in a hook or cron job

Log id, HOME, PATH, current directory, and the absolute Git and SSH paths from the failing process. Configure an explicit SSH IdentityFile, use absolute paths in the hook, and grant that account access to every output and log directory.

The hook receives the push but checkout reports permission denied

Check whether an earlier root-run checkout created root-owned files. Restore ownership to the deploy account, verify the work tree and .git are writable, and make sure the hook itself has execute permission.

CI reports Permission denied (13) during transfer

Verify the public key in the target account’s authorized_keys, the remote destination owner, and whether the transfer requests timestamps or other metadata the filesystem rejects. Test a small file transfer as the CI account before retrying the full deployment.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A key works for one repository but not another

Check repository scope and access mode. Deploy keys are not automatically reusable across repositories; attach the correct key to the intended repository or use a managed identity with explicit access to both.

Permission-safe deployment checklist

  • Classify the message as remote SSH rejection or local filesystem denial.
  • Capture the exact command, account, home directory, path, and environment that failed.
  • Use git remote -v and ssh -vT to confirm host, username, and selected key.
  • Run Git as a dedicated non-root account; avoid sudo git.
  • Inspect ownership and modes on the checkout, .git, hooks, logs, destination, and every parent directory.
  • Correct ownership narrowly and avoid world-writable permissions.
  • Keep web-server read access separate from repository write access.
  • Make hooks executable, deterministic, and explicit about paths, branch, environment, and logging.
  • For CI, match the private key, authorized_keys entry, remote user, and destination permissions.
  • After the fix, verify the deployed commit and review logs from the same account that performs deployment.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.