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
Antihost offers Linux VPS plans with full root access, unlimited traffic on a 1 Gbps port and a 14-day free trial.
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.
Recommended Free Tools
| # | Preview | Product | Price | |
|---|---|---|---|---|
| 1 |
|
Pro Git | Buy on Amazon | |
| 2 |
|
Pro Git (Expert's Voice in Software Development) | $37.07 | Buy on Amazon |
| 3 |
|
Professional Git | $24.79 | Buy on Amazon |
| 4 |
|
Pro Git 1st (first) edition Text Only | $83.26 | Buy on Amazon |
| 5 |
|
Git: Github Programming, In 8 Hours, For Beginners, Learn Coding Fast: Git Github Language, Crash... | $2.99 | Buy on Amazon |
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.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →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:
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:
Rank #2
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.
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/.sshinstead 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.
Rank #3
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:
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchPC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11findmnt -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.
Install and permission the hook
- Create or use a bare repository owned by the deploy account, such as
/srv/git/app.git. - Place the hook at
/srv/git/app.git/hooks/post-receive. - Make it executable:
chmod 750 /srv/git/app.git/hooks/post-receive. - 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.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsFix 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.
Best Value
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.
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.
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.
Quick Recap
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 -vandssh -vTto 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_keysentry, 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.

