ScriptRunner allows you to easily write scripts to handle pre-receive events, which fire when a user pushes changes but before they are accepted in to the repository. The principle purpose of this type of hook is to block the push request and return a message to the user, if certain conditions are not fulfilled.

You can use the built-in content in conjunction with conditions to enforce your workflow. Typical examples of this would be:

Adding a Pre-Receive Hook

Navigate to Admin → Script Pre Hooks. Click a heading to add a handler. Choose Custom script hook to use your own scripts to decide whether to allow the push or not.

Built-in Pre-Receive Hooks

Diagnosable conditions

Most of the built-in pre-receive hooks are applied based on an arbitrary condition. Diagnosable conditions allow you to show the user exactly why the hook was applied for their pushed changes in the hook error message.

One use of this is in the protect git references hook to show users what changes caused the hook to block their push.

For example you could use the following condition to verify all the commit authors are valid Bitbucket users and display the ones who aren’t in the hook error message:

import com.atlassian.bitbucket.user.ApplicationUser
import com.atlassian.bitbucket.user.Person

def authors = refChanges.getCommitAuthors(repository)
authors.findAll {
    if (! (it instanceof ApplicationUser)) {
        def person = it as Person
        hookMessage << "\$ - \$person.emailAddress is not a Bitbucket user\\n".toString()
        return true

    return false

If you use pathsMatch and other variations of it you can use it as a diagnosable condition to automatically display the blocked paths and changes using the version here.

You could even add instructions or Git commands to the error message so the user can fix their pushed changes before pushing again.

Protect git references

A git reference is a pointer to a particular commit. In the case of branches it is a pointer to the head of a line of work. For a tag, it’s a pointer to a particular commit that remains fixed. The purpose of this hook is to block changes to refs based on arbitrary conditions.

So, this does nothing other than reject the commit with the supplied message, if the provided condition evaluates to true. So, as a blank condition evaluates to true, unless you provide a condition this will reject everything (so not very useful).

The error message will only be used if one is not provided in the condition. If you want a dynamic error message based on the changes a user has pushed then you should use diagnosable conditions.

Click the Expand examples link to show examples…​ you will probably need to modify them to suit.

For example to prevent deletion of tags that begin with REL, you would start with the existing example for blocking all tag deletion, and end up with:

import com.atlassian.bitbucket.repository.RefChangeType

refChanges.any {"refs/tags/REL") &&
       it.type == RefChangeType.DELETE

To reject changes to certain subdirectories from users not in a particular authorised group, we need to combine two of the example conditions. Firstly, checking the group of the pusher, and secondly, checking if the user is in a group.

This leads us to the following condition script:

import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.bitbucket.auth.AuthenticationContext
import com.atlassian.bitbucket.user.UserService

def userService = ComponentLocator.getComponent(UserService)
def authContext = ComponentLocator.getComponent(AuthenticationContext)

if (pathsMatchExcludingDeletes('glob:path/to/sensitive/code/**')) {
    // return true if the user is NOT in the desired group, thus blocking the push
    return ! userService.isUserInGroup(authContext.getCurrentUser(), "authorised-devs")
return false
The intention of the examples is that you use them as starting points for more complex instructions, by and and or ing them together for instance.
When restricting pushes by file name, use pathsMatchExcludingDeletes rather than pathsMatch, as this will allow users to delete undesirable files that were in the repository before you started using this hook.

Exclude files tracked by Git LFS

You can exclude files tracked by Git LFS from any hook condition by using the following:

    .excludingLfsFiles() (1)
    .matches("glob:**.jar") (2)
1 tell pathsMatcher to exclude files tracked by Git LFS
2 see if any files in the push (not already by Git LFS) with the jar file extension are present
This is particularly useful if you want a condition to block binary files, but exclude the ones already tracked by Git LFS. You could then add an error message informing the developer that they need to move these files to Git LFS if they want to push them.

You can find further configuration options for pathsMatcher here.

Naming standard enforcement

This allows you to enforce a naming standard for branches and tags using regular expressions.

You may want different naming standards for different types of branches. For example for your feature and bugfix branches, you may want them to contain a JIRA issue ID. For hotfix branches you may require that they begin with a Change Request ID. Therefore you can specify multiple regular expressions for both branches and tags. If none of them match, the supplied error message (or a default one), will be returned to the user.

An example where feature branches must start with a JIRA issue key, hotfix branches must begin with a CR number:


To enforce tag names follow the format: 3.2.14:

This does not apply to branches and tags created through the Bitbucket user interface. You will need to use the separate event handler for this case.

Restrict file size

Allows you to restrict the maximum file size of an individual file. This is important because once pushed and accepted, all files will stay in the repo forever. It will be transmitted to anyone that clones the repo, increasing the amount of time for the checkout operation, and increasing their disk space requirements.

> git push
Counting objects: 5, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 258.45 KiB | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
remote: =====================================================================
remote: File too large (max size 1048576 bytes): largefile.bin
remote: =====================================================================
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to ''

This can be combined with a condition so that certain users are allowed to push large files.

This can take some time when pushing a new repository, as the size of every file in each commit needs to be checked. During testing we were able to push the Groovy Core repository in around 1 minute on a fresh instance of Bitbucket Server. This type of performance can be expected from version 4.3.17 onwards due to this fix: SRBITB-187.
To check the progress of a large push you can enable debug logging for com.onresolve.scriptrunner.canned.bitbucket.hooks.MaxFilesizeHook. It will show you in the logs how many commits have been processed so far for a particular repository.

Exclude files tracked by Git LFS

You can exclude files tracked by Git LFS from the restrict file size hook by selecting the Exclude LFS files checkbox.

Using the restrict file size hook in this way will indicate to the developer which files need to be moved to LFS.

Enforce trusted commit authors

Allows you to specify a policy for handling authorship of incoming commits. This function supports three different policies:

  • All authors must be Bitbucket users

  • All authors must have write access to this repository

  • Can only push own commits

Optionally, you can check the following checkboxes:

  • Allow commits from forks. This will bypass checking for those commits that have already been pushed to forked repositories of the current one. This allows for a process where you have multiple forks with different access controls, and users need to pull from other forks and push back to "their own" one.

  • Commits must match name. This will check the Author name in the pushed commits against the current Bitbucket display name when using Can only push own commits or All authors must be Bitbucket users. Note that by default Bitbucket maps users by email address, so this option is for enforcing the display name matches as well.

For Commits must match name, merged and rebased commits will be ignored, as they are already in the repository. However cherry-picked commits will be matched against committer name and email but we will still check the author is a known Bitbucket user by email.

This is designed to mitigate against the following real-world issues:

Failure to set up the gitconfig with user and email correctly

This will make it hard to trace commits back to the real user. By default, anyone can commit using git commit -m "some comment" --author "Linus Torvalds <>", and if you have write access to the repository, you can push that.

Commits to the repository from someone who doesn’t have write access

A user who does have write access can take a patch (git format-patch) and apply it (git am), from a user who does not have write access. If you are in a regulated environment you may have to explain how that happened, possibly years after the fact. The same thing can be done with git bundle and various other ways.

Creating malevolent commits on behalf of another user

It’s possible for a disgruntled user to create a "bad" commit with another user as the author and push that.

The first two policies prevent primarily against misconfiguration and inadvertent breaches in policy, but may not prevent someone determined to cause problems.

The third policy is more restrictive but will prevent against even a wilful attempt to breach policy. It has a caveat however - although you can still merge other people’s branches and push them, you would not be able to merge from someone elses fork (unless they had already pushed their changes to the Bitbucket repository). You could also not use a shared local repository (which is not a good practice anyway).

Signed commits would be the next level of enforcement, although this can require significant investment in training and infrastructure.

Reject force push

This is the same as the per-repo hook that is in the base product. It is reproduced here as it allows system administrators to apply it at a level above project/repository administrator, such that they cannot disable it.

A force push is a push that rewrites history on the remote server, for instance squashing multiple commits into a single commit (such as is produced by a git rebase). Generally, it’s desirable to squash commits on a feature branch so you have a clear and coherent history for your project…​ but only if the feature branch is not shared, or, it’s shared and you can ensure that the team has pushed any pending changes, and know how to reset from the rewritten branch.

Therefore you might want to combine this hook with conditions, such that you disable force pushing just to the master and release branches for instance, or allow only on feature branches.

prevent force push on master

Reject merge commits

Enforces a linear workflow policy. This means that any unnecessary merge commits will be rejected. This forces your users to use rebase rather than merge to keep a linear history. This does not affect pull requests, so branches can still be merged, however, the trivial merge commits will be avoided. You can set up your own custom message to appear as the push is rejected.

The hook will list the commit ids that were rejected so that the user knows the commits that are causing the push to be blocked.

> git push
Counting objects: 12, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (12/12), done.
Writing objects: 100% (12/12), 1.06 KiB | 0 bytes/s, done.
Total 12 (delta 6), reused 0 (delta 0)
remote: =====================================================================
remote: Rejecting push due to one or more merge commits:
remote: 6378ee0e4e4
remote: ef2dc27692b
remote: Merge commits are not allowed. You should rebase rather than merge.
remote: =====================================================================
To /bitbucket/scm/project_1/repetition.git
! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to '/bitbucket/scm/project_1/repetition.git
This will NOT affect any existing merges, if a merge is already committed and pushed, the hook will not look at that commit, only at new ones.

Require commits to be associated with a JIRA issue

This requires each commit to reference a valid JIRA issue, where the supplied JQL query defines what’s valid. The issue can either be mentioned in the commit message, or be present in the branch name.

If you use a pull request workflow, there is a merge check version of this which is simpler to use.
A configured JIRA application link is required. You either have to make one of your application links primary or select one when configuring this hook, as in the image below. If you select multiple application links then each one will be queried until one returns issues which match the specified JQL clause.
jira app link select
If you remove an application link you have selected for the hook, you should also update the hook configuration. As the hook verifies the application link can be found. If you don’t the hook will fail when you push to it.
Unfortunately the Bitbucket UI will only extract issues where they are referenced by commit message, which is a shame as if you create the branch from the issue, you’d want all commits on that branch to relate to the issue.

A sample query might be:

  • The issue is "in progress" and assigned to the user pushing the changes:

status = "In Progress" and assignee = currentUser()
  • You could also require that each commit references a particular JIRA project:

project = FOO

If the hook rejects the push, the user will need to edit the commit comments (for instance using git rebase) before pushing again.

If you want to exclude merge commits from being checked then you should select the exclude merge commits checkbox when configuring the hook.

You can also build a dynamic JQL clause to query on. You can either create your own templates or use one of the samples that are included in the default installation of ScriptRunner.

Finally, if your Jira issue keys do not follow the following default regex format, resulting in for example JIR-1 being validated by the hook:


You can specify your own issue key regular expression using the "Issue key regex" field.

An example, which would result in JIR.1 being validated by the hook is:


Working with Custom Pre-Receive Hooks

To write your own hook use Custom script hook. Enter the path to your groovy script file, or enter the script inline as usual.

A complete example of a hook that rejects everything is:

hookResponse.out().print("You cannot commit at the moment.") (1)
return false (2)
1 Provide an "informative" message that is displayed to the user
2 Return false to block the push, true to allow it.

A very slightly more complex example, where we check the name of the repository. If the repository is test then the commit is blocked:

import com.atlassian.bitbucket.repository.RefChange

import com.atlassian.bitbucket.repository.Repository

Repository repository = repository (2)
Collection<RefChange> refChanges = refChanges
HookResponse hookResponse = hookResponse

if ( == "test") {
    hookResponse.out().print("You cannot commit at the moment to any repository named 'test'")
    return false
return true
1 Required imports for the objects passed in the script binding.
2 Strongly type objects passed in the binding.

This example also redefines the objects passed in the script binding - this can be useful when working with an IDE, so that it can provide coding assistance. If you are not using an IDE, both of these sections are redundant.

It can be difficult for an end-user to pick out the reason for the failure amongst the response:

Counting objects: 5, done.
Writing objects: 100% (3/3), 256 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
remote: You cannot commit at the moment to any repository named 'test'
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to ''

Therefore, you can use a utility method to call out the error in the response:

import com.onresolve.scriptrunner.canned.bitbucket.util.BitbucketCannedScriptUtils

if ( == "test") {
    def msg = "You cannot commit at the moment to any repository named 'test'"
    hookResponse.out().print(BitbucketCannedScriptUtils.wrapHookResponse(new StringBuilder(msg)))
    return false

This produces a response that is easier to read:

Counting objects: 5, done.
Writing objects: 100% (3/3), 258 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
remote: =====================================================================
remote: You cannot commit at the moment to any repository named 'test'
remote: =====================================================================
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to ''
It may be useful to include a link to an internal wiki article in the response, which can explain how to fix the problem.

As with all extension points, you can modify the file and repeat your push, without changing anything in the UI. The script will be automatically recompiled if it has changed, which makes the development process very fast.

When your script is successfully blocking pushes you don’t want, make sure it also allows the pushes that are OK.

For how-to questions please ask on Atlassian Answers where there is a very active community. Adaptavist staff are also likely to respond there.

Ask a question about ScriptRunner for JIRA, for for Bitbucket Server, or for Confluence.