Skip to main content
Redmine’s hook system lets plugins react to events and inject content into views without modifying any core file. A hook is simply a named extension point that Redmine calls at a specific moment; your plugin provides a listener that responds to it.

How hooks work

When Redmine encounters a hook call it:
  1. Looks up every registered listener that implements a method with the hook’s name.
  2. Calls that method on each listener, passing a context hash with relevant objects.
  3. Collects the return values and, in view hooks, concatenates them into the HTML output.
# lib/redmine/hook.rb (simplified)
module Redmine
  module Hook
    def self.call_hook(hook, context = {})
      [].tap do |response|
        hls = hook_listeners(hook)
        if hls.any?
          hls.each { |listener| response << listener.send(hook, context) }
        end
      end
    end
  end
end
In views, call_hook is available as a helper method that joins all listener responses into a single html_safe string:
<%# The return value is inserted directly into the HTML %>
<%= call_hook(:view_issues_show_details_bottom, issue: @issue) %>
In controllers, call_hook returns an array of results:
call_hook(:controller_issues_new_after_save, { issue: @issue, request: request })

Listener types

Inherit from Redmine::Hook::ViewListener when your hook handler needs to render HTML. This base class includes ActionView helpers, URL helpers, and ApplicationHelper so you can build links, render partials, and use Redmine helpers.
# plugins/my_plugin/lib/my_plugin/hooks.rb
class MyPlugin::Hooks < Redmine::Hook::ViewListener
  # Render a partial whenever the issue details page loads
  render_on :view_issues_show_details_bottom,
            partial: 'issues/my_plugin_details'
end
render_on is a convenience macro that generates a hook method that renders the given partial. The entire context hash is passed as local variables to the partial.You can also write the method by hand:
class MyPlugin::Hooks < Redmine::Hook::ViewListener
  def view_issues_show_details_bottom(context = {})
    issue = context[:issue]
    return '' unless issue.custom_field_value(MY_CF_ID).present?

    content_tag(:p, "Plugin data: #{issue.custom_field_value(MY_CF_ID)}")
  end
end
Every listener class must include the Singleton module. Both Redmine::Hook::Listener and Redmine::Hook::ViewListener do this automatically via inheritance — do not call include Singleton yourself.

Registering a listener

Listeners register themselves automatically when they are defined (via the inherited callback in Redmine::Hook::Listener). You just need to make sure the file is loaded. The standard place is inside the plugin’s init.rb:
# plugins/my_plugin/init.rb
Redmine::Plugin.register :my_plugin do
  name 'My Plugin'
  # ...
end

require_dependency 'my_plugin/hooks'
Or place the hook file in plugins/my_plugin/lib/ — it will be autoloaded by Redmine’s plugin loader.

Context hash

Every hook method receives a context hash. Redmine always populates the following keys:
KeyTypeDescription
:projectProject or nilCurrent project
:requestActionDispatch::RequestCurrent HTTP request
:controllerActionController::BaseCurrent controller instance
:hook_callerObjectThe object that called the hook (view or controller)
Individual hooks may add further keys — check the call site in the source to see what is available.

Calling hooks from your own plugin views

You can define and call your own hooks to let other plugins extend your plugin:
<%# plugins/my_plugin/app/views/things/show.html.erb %>
<h2><%= @thing.name %></h2>
<%= call_hook(:view_my_plugin_things_show_bottom, thing: @thing) %>
Other plugins can then listen on :view_my_plugin_things_show_bottom using a ViewListener.

Common view hooks

These hooks are called from Redmine’s built-in views. Hook names follow the pattern view_<controller>_<action>_<location>.
HookLocation
:view_issues_show_details_bottomBelow the issue attributes table on the issue detail page
:view_issues_show_description_bottomBelow the issue description
:view_issues_form_details_topTop of the issue form details section
:view_issues_form_details_bottomBottom of the issue form details section
:view_issues_new_topTop of the new issue form
:view_issues_edit_topTop of the edit issue form
:view_issues_edit_notes_bottomBelow the notes field on the edit form
:view_issues_index_bottomBottom of the issue list
:view_issues_sidebar_issues_bottomBottom of the issues sidebar
:view_issues_sidebar_planning_bottomBottom of the planning sidebar section
:view_issues_sidebar_queries_bottomBottom of the queries sidebar section
:view_issues_bulk_edit_details_bottomBottom of the bulk-edit form
:view_issues_context_menu_startStart of the issue context menu
:view_issues_context_menu_endEnd of the issue context menu
:view_issues_history_journal_bottomBelow each journal entry
:view_issues_history_changeset_bottomBelow each changeset entry
:view_issues_history_time_entry_bottomBelow each time entry in history
HookLocation
:view_projects_formInside the project form
:view_projects_show_leftLeft column of the project overview
:view_projects_show_rightRight column of the project overview
:view_projects_show_sidebar_bottomBottom of the project sidebar
:view_projects_sidebar_queries_bottomBottom of the project queries sidebar
:view_projects_settings_members_table_headerExtra column header in the members table
:view_projects_settings_members_table_rowExtra column cell in the members table
HookLocation
:view_layouts_base_html_headInside <head> — use to add stylesheets or meta tags
:view_layouts_base_body_topImmediately after <body> opens
:view_layouts_base_contentMain content area
:view_layouts_base_body_bottomJust before </body> — use to add scripts
HookLocation
:view_account_loginOn the login form
:view_my_accountOn the My Account page
:view_my_account_contextualContextual area on My Account
:view_my_account_preferencesPreferences section on My Account
:view_my_page_splitcontentSplit content area on My Page
:view_my_page_contextualContextual area on My Page
:view_users_formInside the user administration form
:view_users_form_preferencesUser preferences section in the admin form
HookLocation
:view_settings_general_formInside the general settings form
:view_custom_fields_form_upper_boxTop of the custom field form
:view_timelog_edit_form_bottomBottom of the time log edit form
:view_wiki_show_sidebar_bottomBottom of the wiki sidebar
:view_search_index_options_content_bottomBottom of the search options
:view_welcome_index_leftLeft area of the welcome page
:view_welcome_index_rightRight area of the welcome page
:view_calendars_show_bottomBottom of the calendar view
:view_repositories_show_contextualContextual area on the repository page

Common controller hooks

Controller hooks let you run code at specific points during request processing.
HookWhen
:controller_issues_new_before_saveBefore a new issue is saved
:controller_issues_new_after_saveAfter a new issue is saved
:controller_issues_bulk_edit_before_saveBefore each issue is saved during bulk edit
:controller_account_success_authentication_afterAfter a user successfully authenticates
:controller_timelog_edit_before_saveBefore a time entry is saved
:controller_wiki_edit_after_saveAfter a wiki page is saved
:controller_messages_new_after_saveAfter a new forum message is saved
:controller_messages_reply_after_saveAfter a forum reply is saved
:controller_custom_fields_new_after_saveAfter a custom field is created
:controller_custom_fields_edit_after_saveAfter a custom field is updated
:controller_journals_edit_postAfter a journal entry is edited
:after_plugins_loadedAfter all plugins have finished loading