How to Work Safely with an Untrusted Git Repository

Working with someone else’s Git repository is completely normal. It might be an open-source project, code delivered by a contractor, a take-home assignment, a new project at work, or simply a repository someone has asked you to review.

Cloning it is the obvious first step. From a security perspective, the more interesting question is what starts running after that.

Your editor may load project-specific configuration. A package manager may execute scripts during installation. Builds and tests run additional code. Submodules may pull in more repositories. A coding agent may perform several of these steps automatically. And if you received the entire working directory, including .git, local repository configuration can even change how Git itself behaves.

That does not mean every unfamiliar project needs a dedicated malware-analysis lab. It does mean it is useful to know when you stop merely reading a project and start allowing it to execute code.

Opening a project can mean more than viewing source files

A modern editor or IDE is not just a text viewer. Once a project is opened, it may load workspace-specific settings, run tasks, configure a debugger, initialize a build system, or enable other project-level automation.

This is why development environments increasingly distinguish between trusted and untrusted projects.

VS Code, for example, has Workspace Trust and Restricted Mode. Until you trust a workspace, it restricts features such as tasks, debugging, and some workspace settings. JetBrains IDEs have a similar Safe Mode that lets you inspect source code without automatically running Maven or Gradle imports and other project scripts.

If your editor asks whether you trust an unfamiliar project, it is not merely showing you another dialog to dismiss. That decision can determine which project-controlled features are allowed to run automatically.

For an initial review, it therefore makes sense to keep a restricted or safe mode enabled if your development environment provides one.

Installing dependencies can already execute code

The next important boundary is project setup.

A JavaScript project will often tell you to run something like:

npm install
npm run dev

npm install is not limited to downloading files into node_modules. npm supports lifecycle scripts such as preinstall, install, postinstall, and prepare, and these may run automatically during installation.

These hooks are a legitimate part of the ecosystem and many perfectly normal packages rely on them. But they are still executable code.

Before installing an unfamiliar project, it is therefore worth checking at least its package.json and the scripts it defines.

For an initial inspection, npm can also install dependencies without running package scripts:

npm install --ignore-scripts

This prevents npm from automatically executing lifecycle scripts during installation. It is not a sandbox, and it does not make the project safe. If you later explicitly run npm run dev, npm test, or another script, that command will still execute its target.

What it does give you is a way to separate downloading dependencies from automatically executing additional code during installation.

The same principle applies outside npm. Maven, Gradle, Python packaging systems, Make, Cargo, and other build or package systems all have their own ways of executing code during setup and builds. The exact mechanism changes, but the security question remains the same: what actually runs when I perform the step described as installation, build, or test?

A malicious command does not have to look malicious

I ran into exactly this problem while analyzing a malicious repository sent as part of a fake job offer.

Its README contained a perfectly ordinary instruction:

npm run dev

The project did start. At the same time, malicious code executed in the background, discovered the address of its command-and-control server, downloaded additional code, and began establishing persistence in development tools.

The attacker did not need to convince the victim to run something called run-malware. The malicious behavior was hidden behind a step that someone reviewing a frontend project would normally perform anyway.

I described the full case in I Ran Malware on Purpose. It Found Its Server Address in the Blockchain.

This is why looking only for obviously suspicious commands is not enough. It also matters what an ordinary command launches, imports, downloads, or executes behind the scenes.

A coding agent can walk through the same chain automatically

Coding agents do not create this problem, but they can make the entire sequence much faster and less visible.

If you ask an agent to “review this project and explain how it works,” it may be able to stay entirely within static analysis. But a task such as “get the project running,” “fix the failing tests,” “find the bug,” or “verify that the application works” may legitimately require it to install dependencies, run a build, execute tests, start a development server, or launch other tools defined by the project.

The agent does not have to violate your instructions. It can follow them exactly and still execute a malicious install or build script.

That is why limiting the environment in which the agent operates is more useful than relying on it to correctly identify every dangerous command.

Claude Code, for example, separates tool permissions from sandboxing. Permissions determine which tools the agent may use, while the sandbox can restrict filesystem and network access for Bash and the processes Bash launches. That means the same boundaries can also apply to npm, build scripts, test runners, and other child processes started by the agent.

The sandbox can be configured from Claude Code with:

/sandbox

For a genuinely untrusted project, sandboxing is most useful when combined with restricted access to sensitive files and unnecessary network destinations. Requiring confirmation for every individual command is a weaker control on its own. A long sequence of otherwise routine-looking commands can also lead to approval fatigue, where prompts are eventually confirmed almost automatically.

One repository can pull in more repositories

Git submodules allow one repository to reference additional Git repositories. Their configuration lives in .gitmodules, which is part of the repository itself.

That makes these two commands meaningfully different:

git clone https://example.com/project.git

and:

git clone --recurse-submodules https://example.com/project.git

The second form immediately starts resolving and cloning additional repositories referenced by the project.

With an unfamiliar repository, there is little reason to initialize submodules before checking where they point. You can inspect .gitmodules first and initialize the submodules only if you actually need them.

Git also has protocol policies for network operations triggered indirectly. Some protocols are allowed by default, the more dangerous ext protocol is disabled, and others may use the user policy, which distinguishes between a transport explicitly requested by the user and one triggered automatically, for example by a submodule.

There is usually no reason to loosen these defaults globally. If a particular project requires broader protocol permissions, it is better to understand why before changing them.

Receiving someone else’s .git is a different situation

So far, we have assumed that you clone a normal remote repository yourself.

The situation changes if someone gives you the entire working directory, for example as a ZIP archive, through a shared drive, or in a synchronized folder:

project/
├── src/
├── package.json
├── README.md
└── .git/

In that case, you did not receive only the source files and Git history. You also received someone else’s local Git metadata.

This is where the distinction from a normal git clone matters. A remote clone does not copy the source repository’s local .git/config or its hooks. Git creates a new .git directory and new local configuration on your machine.

If someone hands you the entire directory including .git, however, you receive the configuration and hooks they prepared as well.

Git’s own security documentation explicitly warns about this. Repository-local configuration and hooks can cause arbitrary commands to run, which is why normal Git operations directly against an untrusted .git directory should not be treated as safe.

A current example: GitSpawn and core.fsmonitor

The recent GitSpawn research from Manifold Security provides a good example.

Git supports core.fsmonitor, which can help it detect changed files more efficiently. Its value, however, may also point to an external program.

An untrusted .git/config can therefore contain something equivalent to:

[core]
    fsmonitor = <program>

Some coding agents automatically queried the state of a Git repository after opening it. When Git performed an operation that refreshed its index, it used the configured fsmonitor and executed the attacker’s program.

That meant an apparently harmless command such as:

git status

could result in execution of a program specified in the repository’s local configuration.

For this specific path, an automated status check can instead use:

git -c core.fsmonitor=false status

The -c option overrides the configuration value for that Git invocation, so the repository’s core.fsmonitor setting is not used for this command.

A global setting such as:

git config --global core.fsmonitor false

is not a reliable defense against a malicious repository-local value. Repository configuration has higher precedence than global user configuration and can override it.

git -c core.fsmonitor=false status is also not a general-purpose way to “secure Git.” It addresses this specific core.fsmonitor path. Git configuration and hooks provide other ways to influence program execution.

safe.directory is not a list of trusted projects

Git has a protection mechanism for repositories owned by another user.

By default, Git refuses to work with a repository owned by someone else and will not trust its repository-local configuration or hooks. Specific exceptions can be added through safe.directory.

This is useful, for example, on multi-user systems.

It is not a general defense against a malicious repository. If you extract an archive prepared by someone else, the resulting files will normally be owned by your own account. From an ownership perspective, Git has no reason to reject them even though the contents of .git came from an untrusted source.

If Git reports “dubious ownership,” it is therefore a bad idea to solve the problem automatically with:

git config --global --add safe.directory '*'

The * value disables the ownership protection globally. It is better to understand why a particular repository has a different owner and add a specific exception only when necessary.

When you actually need the untrusted .git

Sometimes there is no trusted remote copy and you need to preserve the history, branches, or other Git metadata from the directory you received.

Git recommends not working directly in the original repository in that case. Instead, create a fresh clone from it:

git clone --no-local /path/to/original-repository /path/to/clean-copy

--no-local prevents Git from using the usual local-clone optimizations and creates a new repository through the normal Git transport mechanism. The resulting copy therefore gets a new .git directory instead of inheriting the original repository’s local configuration and hooks.

This is still not an absolute sandbox. The server side of the clone is handled by git-upload-pack, and Git’s own documentation points out that this component has an attack surface of its own. For a genuinely hostile .git directory, Git recommends running that side under an unprivileged user or adding another isolation layer.

If you do not need the history at all, the simpler option is not to use the supplied .git directory in the first place.

You can also restrict automatic bare-repository discovery

Git can work with bare repositories, which contain Git data without a normal working tree.

If you do not use bare repositories in your normal workflow, Git provides a useful hardening option:

git config --global safe.bareRepository explicit

With this setting, Git will not automatically operate on a bare repository that it merely discovers somewhere in the directory structure. The repository has to be selected explicitly.

Git documents this option as a mitigation for attacks in which a cloned project contains another bare repository that a later Git command discovers automatically.

When you really need to run the project

Static inspection only gets you so far. At some point you may need to start the application, reproduce a bug, run tests, or observe its behavior.

At that point you already know you are going to execute untrusted code. The most important question is no longer another individual Git setting, but the environment in which that code will run.

A normal developer account may have access to SSH keys, private repositories, Git and cloud tokens, production .env files, saved sessions, other source trees, or credential agents running in the background.

An unfamiliar project usually needs none of these.

If you do not trust a project but still need to run it, use a disposable or otherwise restricted environment without real credentials and without unnecessary access to host data.

Depending on the risk, that may be a sandbox, a separate user account, an isolated development environment, or a disposable virtual machine.

A container can be part of that setup, but “it runs in Docker” is not the same as strong isolation. Docker bind mounts are writable by default, which allows a process in the container to modify files on the host. Access to the Docker daemon is even more powerful; Docker’s own documentation warns that controlling the daemon effectively grants root-level capabilities on the host.

I would therefore avoid exposing the Docker socket, a full home directory, an SSH agent, or credential directories to an untrusted container. If the project only needs to read host files, a read-only mount is preferable to a writable one.

Network access deserves the same treatment. If the project does not need unrestricted outbound connectivity, there is no reason to provide it automatically.

A VM snapshot is useful for restoring disk state. It is not a defense against data exfiltration. Rolling back a machine does not revoke a token that has already been sent somewhere else.

Practical security baseline

None of the measures below make an untrusted repository trustworthy on their own. They reduce different ways in which a project, Git, or the tools around it can execute code or reach the rest of your system.

Situation What to do Why What it does not solve
Opening an unfamiliar project Use Restricted / Safe Mode if your editor or IDE supports it. Limits automatic tasks, builds, and project-controlled configuration before review. Does not protect you from code you later choose to execute.
Initial npm installation npm install --ignore-scripts Prevents lifecycle scripts from running automatically during installation. It is not a sandbox. An explicit npm run still executes its target script.
Git submodules Do not start with --recurse-submodules; inspect .gitmodules first. The main repository may point to additional code sources. Says nothing about whether the main repository itself is safe.
Git protocol policy Do not loosen protocol.*.allow globally without a specific reason. Git already has security-oriented defaults for automatically triggered transports and submodules. An allowed HTTPS or SSH repository can still contain malicious code.
Repository owned by another user Do not use safe.directory=* globally. Add a specific exception only when necessary. Preserves Git’s ownership protection. Does not protect you from a malicious repository that is already owned by your account.
Bare repositories git config --global safe.bareRepository explicit if you do not normally rely on implicit bare repositories. Restricts automatic discovery of a bare repository embedded in an untrusted project. Does not address normal working-tree repositories or other execution paths.
An entire untrusted .git directory git clone --no-local /source /clean-copy Creates a fresh .git instead of working directly with untrusted local configuration and hooks. git-upload-pack still has its own attack surface; highly untrusted input should be isolated further.
Automated git status against an untrusted .git git -c core.fsmonitor=false status Prevents that command from using a repository-supplied core.fsmonitor program. Addresses only this specific GitSpawn path.
Coding agent Use a sandbox and restrict filesystem and network access. In Claude Code, for example, configure sandboxing through /sandbox. The same restrictions can apply to processes started by the agent, including npm and build scripts. Sandboxing does not replace sensible permissions and review of sensitive actions.
Running genuinely untrusted code Use a disposable or restricted environment without SSH keys, production tokens, cloud credentials, the host Docker socket, or other unnecessary data. Reduces the impact if malicious code actually runs. Does not prevent the malicious program from executing.
Docker Do not expose the Docker socket to untrusted containers; restrict host mounts and make them read-only where possible. The Docker socket provides extremely powerful host access, while writable bind mounts allow changes to host files. A container is not, by itself, a complete security boundary.
Git and surrounding tooling Keep supported versions up to date using the normal update mechanism for your operating system or distribution. Git, package managers, editors, and runtimes all have security vulnerabilities of their own. Updates do not protect against legitimate features being used maliciously exactly as designed.

How much trust does the project actually need?

You do not have to decide once and for all whether an unfamiliar repository is “safe” or “unsafe.”

It is more practical to grant trust gradually.

Reading source files does not require access to your SSH keys. Installing dependencies does not always require running every install hook. A coding agent that only needs to explain the architecture does not automatically need permission to execute arbitrary software. And a project that really does need to run does not have to run under the same account, with the same credentials, as the rest of your development work.

The less trust a particular step requires, the less trust there is a reason to give it.

Need to review the security of a development environment, CI workflow, or coding-agent setup?

If you need a second opinion on a concrete workflow, permission model, or technical security design, we can go through it as part of a Security Consultation. The goal is a practical recommendation for your actual environment, not an unnecessarily broad audit.


Sources


Visual Portfolio, Posts & Image Gallery for WordPress

Infra audit

Infrastructure audit focused on security and privacy.

Corporate Training

Employee training can greatly reduce the risk of a hacker's attack on your company