diff --git a/.travis.yml b/.travis.yml
index 65d917d73..0ea8218a6 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -52,6 +52,10 @@ matrix:
       language: node_js
       python: null
       node_js: "lts/*"
+    - os: linux
+      language: generic
+      env: TESTENV=shellcheck
+      services: docker
   fast_finish: true
 
 cache:
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc
index 34ee4ff7c..16075db27 100644
--- a/doc/changelog.asciidoc
+++ b/doc/changelog.asciidoc
@@ -21,6 +21,8 @@ v1.1.0 (unreleased)
 Added
 ~~~~~
 
+- Initial support for Greasemonkey scripts. There are still some rough edges,
+  but many scripts should already work.
 - There's now a `misc/Makefile` file in releases, which should help
   distributions which package qutebrowser, as they can run something like
   `make -f misc/Makefile DESTDIR="$pkgdir" install` now.
@@ -51,6 +53,9 @@ Added
 - New `:edit-command` command to edit the commandline in an editor.
 - New `tabs.persist_mode_on_change` setting to keep the current mode when
   switching tabs.
+- New `session.lazy_restore` setting which allows to not load pages immediately
+  when restoring a session.
+- New `hist_importer.py` script to import history from Firefox/Chromium.
 
 Changed
 ~~~~~~~
@@ -89,6 +94,8 @@ Changed
   data dir, e.g. `~/.local/share/qutebrowser/js`.
 - The current/default bindings are now shown in the :bind completion.
 - Empty categories are now hidden in the `:open` completion.
+- Search terms for URLs and titles can now be mixed when filtering the
+  completion.
 
 Fixed
 ~~~~~
@@ -107,6 +114,8 @@ Fixed
   in a URL.
 - Using e.g. `-s backend webkit` to set the backend now works correctly.
 - Fixed crash when closing the tab an external editor was opened in.
+- When using `:search-next` before a search is finished, no warning about no
+  results being found is shown anymore.
 
 Deprecated
 ~~~~~~~~~~
diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc
index 95caf24f1..5d026bfca 100644
--- a/doc/help/commands.asciidoc
+++ b/doc/help/commands.asciidoc
@@ -54,6 +54,7 @@ It is possible to run or bind multiple commands by separating them with `;;`.
 |<<follow-selected,follow-selected>>|Follow the selected text.
 |<<forward,forward>>|Go forward in the history of the current tab.
 |<<fullscreen,fullscreen>>|Toggle fullscreen mode.
+|<<greasemonkey-reload,greasemonkey-reload>>|Re-read Greasemonkey scripts from disk.
 |<<help,help>>|Show help about a command or setting.
 |<<hint,hint>>|Start hinting.
 |<<history,history>>|Show browsing history.
@@ -491,6 +492,12 @@ Toggle fullscreen mode.
 ==== optional arguments
 * +*-l*+, +*--leave*+: Only leave fullscreen if it was entered by the page.
 
+[[greasemonkey-reload]]
+=== greasemonkey-reload
+Re-read Greasemonkey scripts from disk.
+
+The scripts are read from a 'greasemonkey' subdirectory in qutebrowser's data directory (see `:version`).
+
 [[help]]
 === help
 Syntax: +:help [*--tab*] [*--bg*] [*--window*] ['topic']+
@@ -1083,7 +1090,7 @@ Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] [*--only-active-win
 Save a session.
 
 ==== positional arguments
-* +'name'+: The name of the session. If not given, the session configured in session_default_name is saved.
+* +'name'+: The name of the session. If not given, the session configured in session.default_name is saved.
 
 
 ==== optional arguments
diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc
index 3f6a8a016..967f6c4c6 100644
--- a/doc/help/settings.asciidoc
+++ b/doc/help/settings.asciidoc
@@ -222,7 +222,8 @@
 |<<qt.highdpi,qt.highdpi>>|Turn on Qt HighDPI scaling.
 |<<scrolling.bar,scrolling.bar>>|Show a scrollbar.
 |<<scrolling.smooth,scrolling.smooth>>|Enable smooth scrolling for web pages.
-|<<session_default_name,session_default_name>>|Name of the session to save by default.
+|<<session.default_name,session.default_name>>|Name of the session to save by default.
+|<<session.lazy_restore,session.lazy_restore>>|Load a restored tab as soon as it takes focus.
 |<<spellcheck.languages,spellcheck.languages>>|Languages to use for spell checking.
 |<<statusbar.hide,statusbar.hide>>|Hide the statusbar unless a message is shown.
 |<<statusbar.padding,statusbar.padding>>|Padding (in pixels) for the statusbar.
@@ -2556,8 +2557,8 @@ Type: <<types,Bool>>
 
 Default: +pass:[false]+
 
-[[session_default_name]]
-=== session_default_name
+[[session.default_name]]
+=== session.default_name
 Name of the session to save by default.
 If this is set to null, the session which was last loaded is saved.
 
@@ -2565,6 +2566,14 @@ Type: <<types,SessionName>>
 
 Default: empty
 
+[[session.lazy_restore]]
+=== session.lazy_restore
+Load a restored tab as soon as it takes focus.
+
+Type: <<types,Bool>>
+
+Default: +pass:[false]+
+
 [[spellcheck.languages]]
 === spellcheck.languages
 Languages to use for spell checking.
diff --git a/doc/install.asciidoc b/doc/install.asciidoc
index 15718ec41..312e53176 100644
--- a/doc/install.asciidoc
+++ b/doc/install.asciidoc
@@ -35,7 +35,7 @@ Ubuntu 16.04 doesn't come with an up-to-date engine (a new enough QtWebKit, or
 QtWebEngine). However, it comes with Python 3.5, so you can
 <<tox,install qutebrowser via tox>>.
 
-Debian Stretch / Ubuntu 17.04 and newer
+Debian Stretch / Ubuntu 17.04 and 17.10
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 Those versions come with QtWebEngine in the repositories. This makes it possible
@@ -54,7 +54,18 @@ Install the packages:
 # apt install ./qutebrowser_*_all.deb
 ----
 
-Some additional hints:
+Debian Testing / Ubuntu 18.04
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+On Debian Testing, qutebrowser is in the official repositories, and you can
+install it with apt:
+
+----
+# apt install qutebrowser
+----
+
+Additional hints
+~~~~~~~~~~~~~~~~
 
 - Alternatively, you can <<tox,install qutebrowser via tox>> to get a newer
   QtWebEngine version.
diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt
index 119891725..46905f497 100644
--- a/misc/requirements/requirements-tests.txt
+++ b/misc/requirements/requirements-tests.txt
@@ -13,7 +13,7 @@ glob2==0.6
 hunter==2.0.2
 hypothesis==3.40.1
 itsdangerous==0.24
-# Jinja2==2.9.6
+# Jinja2==2.10
 Mako==1.0.7
 # MarkupSafe==1.0
 parse==1.8.2
diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt
index 1308c8afd..d2b3a719b 100644
--- a/misc/requirements/requirements-tox.txt
+++ b/misc/requirements/requirements-tox.txt
@@ -2,5 +2,6 @@
 
 pluggy==0.6.0
 py==1.5.2
+six==1.11.0
 tox==2.9.1
 virtualenv==15.1.0
diff --git a/misc/userscripts/cast b/misc/userscripts/cast
index da68297d8..f7b64df70 100755
--- a/misc/userscripts/cast
+++ b/misc/userscripts/cast
@@ -144,7 +144,7 @@ fi
 pkill -f "${program_}"
 
 # start youtube download in stream mode (-o -) into temporary file
-youtube-dl -qo - "$1" > ${file_to_cast} &
+youtube-dl -qo - "$1" > "${file_to_cast}" &
 ytdl_pid=$!
 
 msg info "Casting $1" >> "$QUTE_FIFO"
@@ -153,4 +153,4 @@ tail -F "${file_to_cast}" | ${program_} -
 
 # cleanup remaining background process and file on disk
 kill ${ytdl_pid}
-rm -rf ${tmpdir}
+rm -rf "${tmpdir}"
diff --git a/misc/userscripts/dmenu_qutebrowser b/misc/userscripts/dmenu_qutebrowser
index 9c809d5ad..82e6d2f18 100755
--- a/misc/userscripts/dmenu_qutebrowser
+++ b/misc/userscripts/dmenu_qutebrowser
@@ -41,7 +41,7 @@
 [ -z "$QUTE_URL" ] && QUTE_URL='http://google.com'
 
 url=$(echo "$QUTE_URL" | cat - "$QUTE_CONFIG_DIR/quickmarks" "$QUTE_DATA_DIR/history" | dmenu -l 15 -p qutebrowser)
-url=$(echo "$url" | sed -E 's/[^ ]+ +//g' | egrep "https?:" || echo "$url")
+url=$(echo "$url" | sed -E 's/[^ ]+ +//g' | grep -E "https?:" || echo "$url")
 
 [ -z "${url// }" ] && exit
 
diff --git a/misc/userscripts/format_json b/misc/userscripts/format_json
index f756850f1..0d476b327 100755
--- a/misc/userscripts/format_json
+++ b/misc/userscripts/format_json
@@ -1,4 +1,5 @@
 #!/bin/sh
+set -euo pipefail
 #
 # Behavior:
 #   Userscript for qutebrowser which will take the raw JSON text of the current
@@ -19,29 +20,23 @@
 #
 # Bryan Gilbert, 2017
 
+# do not run pygmentize on files larger than this amount of bytes
+MAX_SIZE_PRETTIFY=10485760  # 10 MB
 # default style to monokai if none is provided
 STYLE=${1:-monokai}
-# format json using jq
-FORMATTED_JSON="$(cat "$QUTE_TEXT" | jq '.')"
 
-# if jq command failed or formatted json is empty, assume failure and terminate
-if [ $? -ne 0 ] || [ -z "$FORMATTED_JSON" ]; then
-    echo "Invalid json, aborting..."
-    exit 1
+TEMP_FILE="$(mktemp)"
+jq . "$QUTE_TEXT" >"$TEMP_FILE"
+
+# try GNU stat first and then OSX stat if the former fails
+FILE_SIZE=$(
+  stat --printf="%s" "$TEMP_FILE" 2>/dev/null ||
+  stat -f%z "$TEMP_FILE" 2>/dev/null
+)
+if [ "$FILE_SIZE" -lt "$MAX_SIZE_PRETTIFY" ]; then
+    pygmentize -l json -f html -O full,style="$STYLE" <"$TEMP_FILE" >"${TEMP_FILE}_"
+    mv -f "${TEMP_FILE}_" "$TEMP_FILE"
 fi
 
-# calculate the filesize of the json document
-FILE_SIZE=$(ls -s --block-size=1048576 "$QUTE_TEXT" | cut -d' ' -f1)
-
-# use pygments to pretty-up the json (syntax highlight) if file is less than 10MB
-if [ "$FILE_SIZE" -lt "10" ]; then
-    FORMATTED_JSON="$(echo "$FORMATTED_JSON" | pygmentize -l json -f html -O full,style=$STYLE)"
-fi
-
-# create a temp file and write the formatted json to that file
-TEMP_FILE="$(mktemp --suffix '.html')"
-echo "$FORMATTED_JSON" > $TEMP_FILE
-
-
 # send the command to qutebrowser to open the new file containing the formatted json
 echo "open -t file://$TEMP_FILE" >> "$QUTE_FIFO"
diff --git a/misc/userscripts/open_download b/misc/userscripts/open_download
index 6c1213b65..ecc1d7209 100755
--- a/misc/userscripts/open_download
+++ b/misc/userscripts/open_download
@@ -76,6 +76,7 @@ crop-first-column() {
 ls-files() {
     # add the slash at the end of the download dir enforces to follow the
     # symlink, if the DOWNLOAD_DIR itself is a symlink
+    # shellcheck disable=SC2010
     ls -Q --quoting-style escape    -h  -o -1 -A -t "${DOWNLOAD_DIR}/" \
         | grep '^[-]' \
         | cut -d' ' -f3- \
@@ -91,10 +92,10 @@ if [ "${#entries[@]}" -eq 0 ] ; then
     die "Download directory »${DOWNLOAD_DIR}« empty"
 fi
 
-line=$(printf "%s\n" "${entries[@]}"  \
+line=$(printf '%s\n' "${entries[@]}"  \
        | crop-first-column 55 \
        | column -s $'\t' -t \
-       | $ROFI_CMD "${rofi_default_args[@]}" $ROFI_ARGS) || true
+       | $ROFI_CMD "${rofi_default_args[@]}" "$ROFI_ARGS") || true
 if [ -z "$line" ];  then
     exit 0
 fi
diff --git a/misc/userscripts/password_fill b/misc/userscripts/password_fill
index af394ac2c..5f30a6bf6 100755
--- a/misc/userscripts/password_fill
+++ b/misc/userscripts/password_fill
@@ -64,7 +64,7 @@ die() {
 javascript_escape() {
     # print the first argument in an escaped way, such that it can safely
     # be used within javascripts double quotes
-    sed "s,[\\\'\"],\\\&,g" <<< "$1"
+    sed "s,[\\\\'\"],\\\\&,g" <<< "$1"
 }
 
 # ======================================================= #
@@ -178,7 +178,7 @@ choose_entry_menu() {
     if [ "$nr" -eq 1 ] && ! ((menu_if_one_entry)) ; then
         file="${files[0]}"
     else
-        file=$( printf "%s\n" "${files[@]}" | "${MENU_COMMAND[@]}" )
+        file=$( printf '%s\n' "${files[@]}" | "${MENU_COMMAND[@]}" )
     fi
 }
 
@@ -236,7 +236,7 @@ pass_backend() {
         if ((match_line)) ; then
             # add entries with matching URL-tag
             while read -r -d "" passfile ; do
-                if $GPG "${GPG_OPTS}" -d "$passfile" \
+                if $GPG "${GPG_OPTS[@]}" -d "$passfile" \
                      | grep --max-count=1 -iE "${match_line_pattern}${url}" > /dev/null
                 then
                     passfile="${passfile#$PREFIX}"
@@ -269,7 +269,7 @@ pass_backend() {
                     break
                 fi
             fi
-        done < <($GPG "${GPG_OPTS}" -d "$path" )
+        done < <($GPG "${GPG_OPTS[@]}" -d "$path" )
     }
 }
 # =======================================================
@@ -283,7 +283,7 @@ secret_backend() {
     query_entries() {
         local domain="$1"
         while read -r line ; do
-            if [[ "$line" =~ "attribute.username = " ]] ; then
+            if [[ "$line" == "attribute.username ="* ]] ; then
                 files+=("$domain ${line#${BASH_REMATCH[0]}}")
             fi
         done < <( secret-tool search --unlock --all domain "$domain" 2>&1 )
@@ -303,6 +303,7 @@ pass_backend
 QUTE_CONFIG_DIR=${QUTE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/qutebrowser/}
 PWFILL_CONFIG=${PWFILL_CONFIG:-${QUTE_CONFIG_DIR}/password_fill_rc}
 if [ -f "$PWFILL_CONFIG" ] ; then
+    # shellcheck source=/dev/null
     source "$PWFILL_CONFIG"
 fi
 init
@@ -311,7 +312,7 @@ simplify_url "$QUTE_URL"
 query_entries "${simple_url}"
 no_entries_found
 # remove duplicates
-mapfile -t files < <(printf "%s\n" "${files[@]}" | sort | uniq )
+mapfile -t files < <(printf '%s\n' "${files[@]}" | sort | uniq )
 choose_entry
 if [ -z "$file" ] ; then
     # choose_entry didn't want any of these entries
diff --git a/misc/userscripts/qutedmenu b/misc/userscripts/qutedmenu
index 3f8b13514..de1b8d641 100755
--- a/misc/userscripts/qutedmenu
+++ b/misc/userscripts/qutedmenu
@@ -35,17 +35,12 @@ get_selection() {
 
 # Main
 # https://github.com/halfwit/dotfiles/blob/master/.config/dmenu/font
-if [[ -s $confdir/dmenu/font ]]; then
-    read -r font < "$confdir"/dmenu/font
-fi
+[[ -s $confdir/dmenu/font ]] && read -r font < "$confdir"/dmenu/font
 
-if [[ $font ]]; then
-    opts+=(-fn "$font")
-fi
+[[ $font ]] && opts+=(-fn "$font")
 
-if [[ -s $optsfile ]]; then
-    source "$optsfile"
-fi
+# shellcheck source=/dev/null
+[[ -s $optsfile ]] && source "$optsfile"
 
 url=$(get_selection)
 url=${url/*http/http}
diff --git a/misc/userscripts/rss b/misc/userscripts/rss
index 222d990a2..f8feebee7 100755
--- a/misc/userscripts/rss
+++ b/misc/userscripts/rss
@@ -32,7 +32,7 @@ add_feed () {
     if grep -Fq "$1" "feeds"; then
         notice "$1 is saved already."
     else
-        printf "%s\n" "$1" >> "feeds"
+        printf '%s\n' "$1" >> "feeds"
     fi
 }
 
@@ -57,7 +57,7 @@ notice () {
 
 # Update a database of a feed and open new URLs
 read_items () {
-    cd read_urls
+    cd read_urls || return 1
     feed_file="$(echo "$1" | tr -d /)"
     feed_temp_file="$(mktemp "$feed_file.tmp.XXXXXXXXXX")"
     feed_new_items="$(mktemp "$feed_file.new.XXXXXXXXXX")"
@@ -75,7 +75,7 @@ read_items () {
         cat "$feed_new_items" >> "$feed_file"
         sort -o "$feed_file" "$feed_file"
         rm "$feed_temp_file" "$feed_new_items"
-    fi | while read item; do
+    fi | while read -r item; do
         echo "open -t $item" > "$QUTE_FIFO"
     done
 }
@@ -85,7 +85,7 @@ if [ ! -d "$config_dir/read_urls" ]; then
     mkdir -p "$config_dir/read_urls"
 fi
 
-cd "$config_dir"
+cd "$config_dir" || exit 1
 
 if [ $# != 0 ]; then
     for arg in "$@"; do
@@ -115,7 +115,7 @@ if < /dev/null grep --help 2>&1 | grep -q -- -a; then
     text_only="-a"
 fi
 
-while read feed_url; do
+while read -r feed_url; do
     read_items "$feed_url" &
 done < "$config_dir/feeds"
 
diff --git a/misc/userscripts/taskadd b/misc/userscripts/taskadd
index 6add71c68..b1ded245c 100755
--- a/misc/userscripts/taskadd
+++ b/misc/userscripts/taskadd
@@ -25,12 +25,10 @@
 [[ $QUTE_MODE == 'hints' ]] && title=$QUTE_SELECTED_TEXT || title=$QUTE_TITLE
 
 # try to add the task and grab the output
-msg="$(task add $title $@ 2>&1)"
-
-if [[ $? == 0 ]]; then
+if msg="$(task add "$title" "$*" 2>&1)"; then
     # annotate the new task with the url, send the output back to the browser
     task +LATEST annotate "$QUTE_URL"
-    echo "message-info '$msg'" >> $QUTE_FIFO
+    echo "message-info '$msg'" >> "$QUTE_FIFO"
 else
-    echo "message-error '$msg'" >> $QUTE_FIFO
+    echo "message-error '$msg'" >> "$QUTE_FIFO"
 fi
diff --git a/misc/userscripts/view_in_mpv b/misc/userscripts/view_in_mpv
index 9eb6ff7c6..f465fc4e4 100755
--- a/misc/userscripts/view_in_mpv
+++ b/misc/userscripts/view_in_mpv
@@ -50,7 +50,7 @@ msg() {
 MPV_COMMAND=${MPV_COMMAND:-mpv}
 # Warning: spaces in single flags are not supported
 MPV_FLAGS=${MPV_FLAGS:- --force-window --no-terminal --keep-open=yes --ytdl --ytdl-raw-options=yes-playlist=}
-video_command=( "$MPV_COMMAND"  $MPV_FLAGS )
+IFS=" " read -r -a video_command <<< "$MPV_COMMAND $MPV_FLAGS"
 
 js() {
 cat <<EOF
diff --git a/qutebrowser/app.py b/qutebrowser/app.py
index 2ed579f61..7029f8df5 100644
--- a/qutebrowser/app.py
+++ b/qutebrowser/app.py
@@ -64,7 +64,7 @@ from qutebrowser.completion.models import miscmodels
 from qutebrowser.commands import cmdutils, runners, cmdexc
 from qutebrowser.config import config, websettings, configfiles, configinit
 from qutebrowser.browser import (urlmarks, adblock, history, browsertab,
-                                 downloads)
+                                 downloads, greasemonkey)
 from qutebrowser.browser.network import proxy
 from qutebrowser.browser.webkit import cookies, cache
 from qutebrowser.browser.webkit.network import networkmanager
@@ -491,6 +491,9 @@ def _init_modules(args, crash_handler):
     diskcache = cache.DiskCache(standarddir.cache(), parent=qApp)
     objreg.register('cache', diskcache)
 
+    log.init.debug("Initializing Greasemonkey...")
+    greasemonkey.init()
+
     log.init.debug("Misc initialization...")
     macros.init()
     # Init backend-specific stuff
diff --git a/qutebrowser/browser/greasemonkey.py b/qutebrowser/browser/greasemonkey.py
new file mode 100644
index 000000000..9a82d6a93
--- /dev/null
+++ b/qutebrowser/browser/greasemonkey.py
@@ -0,0 +1,224 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# This file is part of qutebrowser.
+#
+# qutebrowser is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# qutebrowser is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with qutebrowser.  If not, see <http://www.gnu.org/licenses/>.
+
+"""Load, parse and make available Greasemonkey scripts."""
+
+import re
+import os
+import json
+import fnmatch
+import functools
+import glob
+
+import attr
+from PyQt5.QtCore import pyqtSignal, QObject, QUrl
+
+from qutebrowser.utils import log, standarddir, jinja, objreg
+from qutebrowser.commands import cmdutils
+
+
+def _scripts_dir():
+    """Get the directory of the scripts."""
+    return os.path.join(standarddir.data(), 'greasemonkey')
+
+
+class GreasemonkeyScript:
+
+    """Container class for userscripts, parses metadata blocks."""
+
+    def __init__(self, properties, code):
+        self._code = code
+        self.includes = []
+        self.excludes = []
+        self.description = None
+        self.name = None
+        self.namespace = None
+        self.run_at = None
+        self.script_meta = None
+        self.runs_on_sub_frames = True
+        for name, value in properties:
+            if name == 'name':
+                self.name = value
+            elif name == 'namespace':
+                self.namespace = value
+            elif name == 'description':
+                self.description = value
+            elif name in ['include', 'match']:
+                self.includes.append(value)
+            elif name in ['exclude', 'exclude_match']:
+                self.excludes.append(value)
+            elif name == 'run-at':
+                self.run_at = value
+            elif name == 'noframes':
+                self.runs_on_sub_frames = False
+
+    HEADER_REGEX = r'// ==UserScript==|\n+// ==/UserScript==\n'
+    PROPS_REGEX = r'// @(?P<prop>[^\s]+)\s*(?P<val>.*)'
+
+    @classmethod
+    def parse(cls, source):
+        """GreasemonkeyScript factory.
+
+        Takes a userscript source and returns a GreasemonkeyScript.
+        Parses the Greasemonkey metadata block, if present, to fill out
+        attributes.
+        """
+        matches = re.split(cls.HEADER_REGEX, source, maxsplit=2)
+        try:
+            _head, props, _code = matches
+        except ValueError:
+            props = ""
+        script = cls(re.findall(cls.PROPS_REGEX, props), source)
+        script.script_meta = props
+        if not props:
+            script.includes = ['*']
+        return script
+
+    def code(self):
+        """Return the processed JavaScript code of this script.
+
+        Adorns the source code with GM_* methods for Greasemonkey
+        compatibility and wraps it in an IFFE to hide it within a
+        lexical scope. Note that this means line numbers in your
+        browser's debugger/inspector will not match up to the line
+        numbers in the source script directly.
+        """
+        return jinja.js_environment.get_template(
+            'greasemonkey_wrapper.js').render(
+                scriptName="/".join([self.namespace or '', self.name]),
+                scriptInfo=self._meta_json(),
+                scriptMeta=self.script_meta,
+                scriptSource=self._code)
+
+    def _meta_json(self):
+        return json.dumps({
+            'name': self.name,
+            'description': self.description,
+            'matches': self.includes,
+            'includes': self.includes,
+            'excludes': self.excludes,
+            'run-at': self.run_at,
+        })
+
+
+@attr.s
+class MatchingScripts(object):
+
+    """All userscripts registered to run on a particular url."""
+
+    url = attr.ib()
+    start = attr.ib(default=attr.Factory(list))
+    end = attr.ib(default=attr.Factory(list))
+    idle = attr.ib(default=attr.Factory(list))
+
+
+class GreasemonkeyManager(QObject):
+
+    """Manager of userscripts and a Greasemonkey compatible environment.
+
+    Signals:
+        scripts_reloaded: Emitted when scripts are reloaded from disk.
+            Any cached or already-injected scripts should be
+            considered obselete.
+    """
+
+    scripts_reloaded = pyqtSignal()
+    # https://wiki.greasespot.net/Include_and_exclude_rules#Greaseable_schemes
+    # Limit the schemes scripts can run on due to unreasonable levels of
+    # exploitability
+    greaseable_schemes = ['http', 'https', 'ftp', 'file']
+
+    def __init__(self, parent=None):
+        super().__init__(parent)
+        self.load_scripts()
+
+    @cmdutils.register(name='greasemonkey-reload',
+                       instance='greasemonkey')
+    def load_scripts(self):
+        """Re-read Greasemonkey scripts from disk.
+
+        The scripts are read from a 'greasemonkey' subdirectory in
+        qutebrowser's data directory (see `:version`).
+        """
+        self._run_start = []
+        self._run_end = []
+        self._run_idle = []
+
+        scripts_dir = os.path.abspath(_scripts_dir())
+        log.greasemonkey.debug("Reading scripts from: {}".format(scripts_dir))
+        for script_filename in glob.glob(os.path.join(scripts_dir, '*.js')):
+            if not os.path.isfile(script_filename):
+                continue
+            script_path = os.path.join(scripts_dir, script_filename)
+            with open(script_path, encoding='utf-8') as script_file:
+                script = GreasemonkeyScript.parse(script_file.read())
+                if not script.name:
+                    script.name = script_filename
+
+                if script.run_at == 'document-start':
+                    self._run_start.append(script)
+                elif script.run_at == 'document-end':
+                    self._run_end.append(script)
+                elif script.run_at == 'document-idle':
+                    self._run_idle.append(script)
+                else:
+                    log.greasemonkey.warning("Script {} has invalid run-at "
+                                             "defined, defaulting to "
+                                             "document-end"
+                                             .format(script_path))
+                    # Default as per
+                    # https://wiki.greasespot.net/Metadata_Block#.40run-at
+                    self._run_end.append(script)
+                log.greasemonkey.debug("Loaded script: {}".format(script.name))
+        self.scripts_reloaded.emit()
+
+    def scripts_for(self, url):
+        """Fetch scripts that are registered to run for url.
+
+        returns a tuple of lists of scripts meant to run at (document-start,
+        document-end, document-idle)
+        """
+        if url.scheme() not in self.greaseable_schemes:
+            return MatchingScripts(url, [], [], [])
+        match = functools.partial(fnmatch.fnmatch,
+                                  url.toString(QUrl.FullyEncoded))
+        tester = (lambda script:
+                  any(match(pat) for pat in script.includes) and
+                  not any(match(pat) for pat in script.excludes))
+        return MatchingScripts(
+            url,
+            [script for script in self._run_start if tester(script)],
+            [script for script in self._run_end if tester(script)],
+            [script for script in self._run_idle if tester(script)]
+        )
+
+    def all_scripts(self):
+        """Return all scripts found in the configured script directory."""
+        return self._run_start + self._run_end + self._run_idle
+
+
+def init():
+    """Initialize Greasemonkey support."""
+    gm_manager = GreasemonkeyManager()
+    objreg.register('greasemonkey', gm_manager)
+
+    try:
+        os.mkdir(_scripts_dir())
+    except FileExistsError:
+        pass
diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py
index 3fb6459a5..e6262a007 100644
--- a/qutebrowser/browser/qutescheme.py
+++ b/qutebrowser/browser/qutescheme.py
@@ -29,6 +29,7 @@ import os
 import time
 import textwrap
 import mimetypes
+import urllib
 
 import pkg_resources
 from PyQt5.QtCore import QUrlQuery, QUrl
@@ -425,6 +426,18 @@ def qute_settings(url):
     return 'text/html', html
 
 
+@add_handler('back')
+def qute_back(url):
+    """Handler for qute://back.
+
+    Simple page to free ram / lazy load a site, goes back on focusing the tab.
+    """
+    html = jinja.render(
+        'back.html',
+        title='Suspended: ' + urllib.parse.unquote(url.fragment()))
+    return 'text/html', html
+
+
 @add_handler('configdiff')
 def qute_configdiff(url):
     """Handler for qute://configdiff."""
diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py
index 4bf525c46..adc7a1034 100644
--- a/qutebrowser/browser/webengine/webenginesettings.py
+++ b/qutebrowser/browser/webengine/webenginesettings.py
@@ -244,6 +244,41 @@ def _init_profiles():
         private_profile.setSpellCheckEnabled(True)
 
 
+def inject_userscripts():
+    """Register user JavaScript files with the global profiles."""
+    # The Greasemonkey metadata block support in QtWebEngine only starts at
+    # Qt 5.8. With 5.7.1, we need to inject the scripts ourselves in response
+    # to urlChanged.
+    if not qtutils.version_check('5.8'):
+        return
+
+    # Since we are inserting scripts into profile.scripts they won't
+    # just get replaced by new gm scripts like if we were injecting them
+    # ourselves so we need to remove all gm scripts, while not removing
+    # any other stuff that might have been added. Like the one for
+    # stylesheets.
+    greasemonkey = objreg.get('greasemonkey')
+    for profile in [default_profile, private_profile]:
+        scripts = profile.scripts()
+        for script in scripts.toList():
+            if script.name().startswith("GM-"):
+                log.greasemonkey.debug('Removing script: {}'
+                                       .format(script.name()))
+                removed = scripts.remove(script)
+                assert removed, script.name()
+
+        # Then add the new scripts.
+        for script in greasemonkey.all_scripts():
+            new_script = QWebEngineScript()
+            new_script.setWorldId(QWebEngineScript.MainWorld)
+            new_script.setSourceCode(script.code())
+            new_script.setName("GM-{}".format(script.name))
+            new_script.setRunsOnSubFrames(script.runs_on_sub_frames)
+            log.greasemonkey.debug('adding script: {}'
+                                   .format(new_script.name()))
+            scripts.insert(new_script)
+
+
 def init(args):
     """Initialize the global QWebSettings."""
     if args.enable_webengine_inspector:
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
index 89ba958a7..813f1eb9c 100644
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -69,6 +69,10 @@ def init():
     download_manager.install(webenginesettings.private_profile)
     objreg.register('webengine-download-manager', download_manager)
 
+    greasemonkey = objreg.get('greasemonkey')
+    greasemonkey.scripts_reloaded.connect(webenginesettings.inject_userscripts)
+    webenginesettings.inject_userscripts()
+
 
 # Mapping worlds from usertypes.JsWorld to QWebEngineScript world IDs.
 _JS_WORLD_MAP = {
@@ -121,18 +125,35 @@ class WebEnginePrinting(browsertab.AbstractPrinting):
 
 class WebEngineSearch(browsertab.AbstractSearch):
 
-    """QtWebEngine implementations related to searching on the page."""
+    """QtWebEngine implementations related to searching on the page.
+
+    Attributes:
+        _flags: The QWebEnginePage.FindFlags of the last search.
+        _pending_searches: How many searches have been started but not called
+                           back yet.
+    """
 
     def __init__(self, parent=None):
         super().__init__(parent)
         self._flags = QWebEnginePage.FindFlags(0)
+        self._pending_searches = 0
 
     def _find(self, text, flags, callback, caller):
         """Call findText on the widget."""
         self.search_displayed = True
+        self._pending_searches += 1
 
         def wrapped_callback(found):
             """Wrap the callback to do debug logging."""
+            self._pending_searches -= 1
+            if self._pending_searches > 0:
+                # See https://github.com/qutebrowser/qutebrowser/issues/2442
+                # and https://github.com/qt/qtwebengine/blob/5.10/src/core/web_contents_adapter.cpp#L924-L934
+                log.webview.debug("Ignoring cancelled search callback with "
+                                  "{} pending searches".format(
+                                      self._pending_searches))
+                return
+
             found_text = 'found' if found else "didn't find"
             if flags:
                 flag_text = 'with flags {}'.format(debug.qflags_key(
diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py
index 56bd1eb5a..b313fc36c 100644
--- a/qutebrowser/browser/webengine/webview.py
+++ b/qutebrowser/browser/webengine/webview.py
@@ -23,12 +23,14 @@ import functools
 
 from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, PYQT_VERSION
 from PyQt5.QtGui import QPalette
-from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
+from PyQt5.QtWebEngineWidgets import (QWebEngineView, QWebEnginePage,
+                                      QWebEngineScript)
 
 from qutebrowser.browser import shared
 from qutebrowser.browser.webengine import certificateerror, webenginesettings
 from qutebrowser.config import config
-from qutebrowser.utils import log, debug, usertypes, jinja, urlutils, message
+from qutebrowser.utils import (log, debug, usertypes, jinja, urlutils, message,
+                               objreg, qtutils)
 
 
 class WebEngineView(QWebEngineView):
@@ -135,6 +137,7 @@ class WebEnginePage(QWebEnginePage):
         self._theme_color = theme_color
         self._set_bg_color()
         config.instance.changed.connect(self._set_bg_color)
+        self.urlChanged.connect(self._inject_userjs)
 
     @config.change_filter('colors.webpage.bg')
     def _set_bg_color(self):
@@ -300,3 +303,43 @@ class WebEnginePage(QWebEnginePage):
             message.error(msg)
             return False
         return True
+
+    @pyqtSlot('QUrl')
+    def _inject_userjs(self, url):
+        """Inject userscripts registered for `url` into the current page."""
+        if qtutils.version_check('5.8'):
+            # Handled in webenginetab with the builtin Greasemonkey
+            # support.
+            return
+
+        # Using QWebEnginePage.scripts() to hold the user scripts means
+        # we don't have to worry ourselves about where to inject the
+        # page but also means scripts hang around for the tab lifecycle.
+        # So clear them here.
+        scripts = self.scripts()
+        for script in scripts.toList():
+            if script.name().startswith("GM-"):
+                log.greasemonkey.debug("Removing script: {}"
+                                       .format(script.name()))
+                removed = scripts.remove(script)
+                assert removed, script.name()
+
+        def _add_script(script, injection_point):
+            new_script = QWebEngineScript()
+            new_script.setInjectionPoint(injection_point)
+            new_script.setWorldId(QWebEngineScript.MainWorld)
+            new_script.setSourceCode(script.code())
+            new_script.setName("GM-{}".format(script.name))
+            new_script.setRunsOnSubFrames(script.runs_on_sub_frames)
+            log.greasemonkey.debug("Adding script: {}"
+                                   .format(new_script.name()))
+            scripts.insert(new_script)
+
+        greasemonkey = objreg.get('greasemonkey')
+        matching_scripts = greasemonkey.scripts_for(url)
+        for script in matching_scripts.start:
+            _add_script(script, QWebEngineScript.DocumentCreation)
+        for script in matching_scripts.end:
+            _add_script(script, QWebEngineScript.DocumentReady)
+        for script in matching_scripts.idle:
+            _add_script(script, QWebEngineScript.Deferred)
diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py
index 7e1d991b9..89407fcdf 100644
--- a/qutebrowser/browser/webkit/webpage.py
+++ b/qutebrowser/browser/webkit/webpage.py
@@ -86,6 +86,21 @@ class BrowserPage(QWebPage):
             self.on_save_frame_state_requested)
         self.restoreFrameStateRequested.connect(
             self.on_restore_frame_state_requested)
+        self.loadFinished.connect(
+            functools.partial(self._inject_userjs, self.mainFrame()))
+        self.frameCreated.connect(self._connect_userjs_signals)
+
+    @pyqtSlot('QWebFrame*')
+    def _connect_userjs_signals(self, frame):
+        """Connect userjs related signals to `frame`.
+
+        Connect the signals used as triggers for injecting user
+        JavaScripts into the passed QWebFrame.
+        """
+        log.greasemonkey.debug("Connecting to frame {} ({})"
+                               .format(frame, frame.url().toDisplayString()))
+        frame.loadFinished.connect(
+            functools.partial(self._inject_userjs, frame))
 
     def javaScriptPrompt(self, frame, js_msg, default):
         """Override javaScriptPrompt to use qutebrowser prompts."""
@@ -283,6 +298,38 @@ class BrowserPage(QWebPage):
         else:
             self.error_occurred = False
 
+    def _inject_userjs(self, frame):
+        """Inject user JavaScripts into the page.
+
+        Args:
+            frame: The QWebFrame to inject the user scripts into.
+        """
+        url = frame.url()
+        if url.isEmpty():
+            url = frame.requestedUrl()
+
+        log.greasemonkey.debug("_inject_userjs called for {} ({})"
+                               .format(frame, url.toDisplayString()))
+
+        greasemonkey = objreg.get('greasemonkey')
+        scripts = greasemonkey.scripts_for(url)
+        # QtWebKit has trouble providing us with signals representing
+        # page load progress at reasonable times, so we just load all
+        # scripts on the same event.
+        toload = scripts.start + scripts.end + scripts.idle
+
+        if url.isEmpty():
+            # This happens during normal usage like with view source but may
+            # also indicate a bug.
+            log.greasemonkey.debug("Not running scripts for frame with no "
+                                   "url: {}".format(frame))
+            assert not toload, toload
+
+        for script in toload:
+            if frame is self.mainFrame() or script.runs_on_sub_frames:
+                log.webview.debug('Running GM script: {}'.format(script.name))
+                frame.evaluateJavaScript(script.code())
+
     @pyqtSlot('QWebFrame*', 'QWebPage::Feature')
     def _on_feature_permission_requested(self, frame, feature):
         """Ask the user for approval for geolocation/notifications."""
diff --git a/qutebrowser/completion/models/histcategory.py b/qutebrowser/completion/models/histcategory.py
index b993b40de..fe89dc79b 100644
--- a/qutebrowser/completion/models/histcategory.py
+++ b/qutebrowser/completion/models/histcategory.py
@@ -47,7 +47,7 @@ class HistoryCategory(QSqlQueryModel):
             "FROM CompletionHistory",
             # the incoming pattern will have literal % and _ escaped with '\'
             # we need to tell sql to treat '\' as an escape character
-            "WHERE (url LIKE :pat escape '\\' or title LIKE :pat escape '\\')",
+            "WHERE ((url || title) LIKE :pat escape '\\')",
             self._atime_expr(),
             "ORDER BY last_atime DESC",
         ]), forward_only=False)
diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml
index efcc07b35..550e58994 100644
--- a/qutebrowser/config/configdata.yml
+++ b/qutebrowser/config/configdata.yml
@@ -79,6 +79,9 @@ new_instance_open_target_window:
     When `new_instance_open_target` is not set to `window`, this is ignored.
 
 session_default_name:
+  renamed: session.default_name
+
+session.default_name:
   type:
     name: SessionName
     none_ok: true
@@ -88,6 +91,11 @@ session_default_name:
 
     If this is set to null, the session which was last loaded is saved.
 
+session.lazy_restore:
+  type: Bool
+  default: false
+  desc: Load a restored tab as soon as it takes focus.
+
 backend:
   type:
     name: String
diff --git a/qutebrowser/html/back.html b/qutebrowser/html/back.html
new file mode 100644
index 000000000..894427800
--- /dev/null
+++ b/qutebrowser/html/back.html
@@ -0,0 +1,60 @@
+{% extends "base.html" %}
+
+{% block script %}
+const STATE_BACK = "back";
+const STATE_FORWARD = "forward";
+
+function switch_state(new_state) {
+	history.replaceState(
+		new_state,
+		document.title,
+		location.pathname + location.hash);
+}
+
+function go_back() {
+	switch_state(STATE_FORWARD);
+	history.back();
+}
+
+function go_forward() {
+	switch_state(STATE_BACK);
+	history.forward();
+}
+
+function prepare_restore() {
+	if (!document.hidden) {
+		go_back();
+		return;
+	}
+
+	document.addEventListener("visibilitychange", go_back);
+}
+
+// there are three states
+// default: register focus listener,
+//	    on focus: go back and switch to the state forward
+// back: user came from a later history entry
+//	 -> switch to the state forward,
+//	    forward him to the previous history entry
+// forward: user came from a previous history entry
+//	 -> switch to the state back,
+//	    forward him to the next history entry
+switch (history.state) {
+	case STATE_BACK:
+		go_back();
+		break;
+	case STATE_FORWARD:
+		go_forward();
+		break;
+	default:
+		setTimeout(prepare_restore, 1000);
+		break;
+}
+{% endblock %}
+
+{% block content %}
+<noscript><p>Javascript isn't enabled. So you need to manually go back in history to restore this tab.</p></noscript>
+<p>Loading suspended page...<br>
+<br>
+If nothing happens, something went wrong or you disabled JavaScript.</p>
+{% endblock %}
diff --git a/qutebrowser/javascript/.eslintignore b/qutebrowser/javascript/.eslintignore
index ca4d3c667..036a72cfe 100644
--- a/qutebrowser/javascript/.eslintignore
+++ b/qutebrowser/javascript/.eslintignore
@@ -1,2 +1,4 @@
 # Upstream Mozilla's code
 pac_utils.js
+# Actually a jinja template so eslint chokes on the {{}} syntax.
+greasemonkey_wrapper.js
diff --git a/qutebrowser/javascript/greasemonkey_wrapper.js b/qutebrowser/javascript/greasemonkey_wrapper.js
new file mode 100644
index 000000000..2d36220dc
--- /dev/null
+++ b/qutebrowser/javascript/greasemonkey_wrapper.js
@@ -0,0 +1,118 @@
+(function() {
+    const _qute_script_id = "__gm_" + {{ scriptName | tojson }};
+
+    function GM_log(text) {
+        console.log(text);
+    }
+
+    const GM_info = {
+        'script': {{ scriptInfo }},
+        'scriptMetaStr': {{ scriptMeta | tojson }},
+        'scriptWillUpdate': false,
+        'version': "0.0.1",
+        // so scripts don't expect exportFunction
+        'scriptHandler': 'Tampermonkey',
+    };
+
+    function checkKey(key, funcName) {
+        if (typeof key !== "string") {
+          throw new Error(`${funcName} requires the first parameter to be of type string, not '${typeof key}'`);
+        }
+    }
+
+    function GM_setValue(key, value) {
+        checkKey(key, "GM_setValue");
+        if (typeof value !== "string" &&
+            typeof value !== "number" &&
+            typeof value !== "boolean") {
+          throw new Error(`GM_setValue requires the second parameter to be of type string, number or boolean, not '${typeof value}'`);
+        }
+        localStorage.setItem(_qute_script_id + key, value);
+    }
+
+    function GM_getValue(key, default_) {
+        checkKey(key, "GM_getValue");
+        return localStorage.getItem(_qute_script_id + key) || default_;
+    }
+
+    function GM_deleteValue(key) {
+        checkKey(key, "GM_deleteValue");
+        localStorage.removeItem(_qute_script_id + key);
+    }
+
+    function GM_listValues() {
+        const keys = [];
+        for (let i = 0; i < localStorage.length; i++) {
+            if (localStorage.key(i).startsWith(_qute_script_id)) {
+                keys.push(localStorage.key(i).slice(_qute_script_id.length));
+            }
+        }
+        return keys;
+    }
+
+    function GM_openInTab(url) {
+        window.open(url);
+    }
+
+
+    // Almost verbatim copy from Eric
+    function GM_xmlhttpRequest(/* object */ details) {
+        details.method = details.method ? details.method.toUpperCase() : "GET";
+
+        if (!details.url) {
+            throw new Error("GM_xmlhttpRequest requires an URL.");
+        }
+
+        // build XMLHttpRequest object
+        const oXhr = new XMLHttpRequest();
+        // run it
+        if ("onreadystatechange" in details) {
+            oXhr.onreadystatechange = function() {
+                details.onreadystatechange(oXhr);
+            };
+        }
+        if ("onload" in details) {
+            oXhr.onload = function() { details.onload(oXhr); };
+        }
+        if ("onerror" in details) {
+            oXhr.onerror = function () { details.onerror(oXhr); };
+        }
+
+        oXhr.open(details.method, details.url, true);
+
+        if ("headers" in details) {
+            for (const header in details.headers) {
+                oXhr.setRequestHeader(header, details.headers[header]);
+            }
+        }
+
+        if ("data" in details) {
+            oXhr.send(details.data);
+        } else {
+            oXhr.send();
+        }
+    }
+
+    function GM_addStyle(/* String */ styles) {
+        const oStyle = document.createElement("style");
+        oStyle.setAttribute("type", "text/css");
+        oStyle.appendChild(document.createTextNode(styles));
+
+        const head = document.getElementsByTagName("head")[0];
+        if (head === undefined) {
+            document.onreadystatechange = function() {
+                if (document.readyState === "interactive") {
+                    document.getElementsByTagName("head")[0].appendChild(oStyle);
+                }
+            };
+        } else {
+            head.appendChild(oStyle);
+        }
+    }
+
+    const unsafeWindow = window;
+
+    // ====== The actual user script source ====== //
+{{ scriptSource }}
+    // ====== End User Script ====== //
+})();
diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py
index 064d8c9e9..bfd73ef32 100644
--- a/qutebrowser/misc/sessions.py
+++ b/qutebrowser/misc/sessions.py
@@ -21,6 +21,8 @@
 
 import os
 import os.path
+import itertools
+import urllib
 
 import sip
 from PyQt5.QtCore import QUrl, QObject, QPoint, QTimer
@@ -205,7 +207,13 @@ class SessionManager(QObject):
         for idx, item in enumerate(tab.history):
             qtutils.ensure_valid(item)
             item_data = self._save_tab_item(tab, idx, item)
-            data['history'].append(item_data)
+            if item.url().scheme() == 'qute' and item.url().host() == 'back':
+                # don't add qute://back to the session file
+                if item_data.get('active', False) and data['history']:
+                    # mark entry before qute://back as active
+                    data['history'][-1]['active'] = True
+            else:
+                data['history'].append(item_data)
         return data
 
     def _save_all(self, *, only_window=None, with_private=False):
@@ -251,7 +259,7 @@ class SessionManager(QObject):
                   object.
         """
         if name is default:
-            name = config.val.session_default_name
+            name = config.val.session.default_name
             if name is None:
                 if self._current is not None:
                     name = self._current
@@ -323,7 +331,18 @@ class SessionManager(QObject):
     def _load_tab(self, new_tab, data):
         """Load yaml data into a newly opened tab."""
         entries = []
-        for histentry in data['history']:
+        lazy_load = []
+        # use len(data['history'])
+        # -> dropwhile empty if not session.lazy_session
+        lazy_index = len(data['history'])
+        gen = itertools.chain(
+            itertools.takewhile(lambda _: not lazy_load,
+                                enumerate(data['history'])),
+            enumerate(lazy_load),
+            itertools.dropwhile(lambda i: i[0] < lazy_index,
+                                enumerate(data['history'])))
+
+        for i, histentry in gen:
             user_data = {}
 
             if 'zoom' in data:
@@ -347,6 +366,20 @@ class SessionManager(QObject):
             if 'pinned' in histentry:
                 new_tab.data.pinned = histentry['pinned']
 
+            if (config.val.session.lazy_restore and
+                    histentry.get('active', False) and
+                    not histentry['url'].startswith('qute://back')):
+                # remove "active" mark and insert back page marked as active
+                lazy_index = i + 1
+                lazy_load.append({
+                    'title': histentry['title'],
+                    'url':
+                        'qute://back#' +
+                        urllib.parse.quote(histentry['title']),
+                    'active': True
+                })
+                histentry['active'] = False
+
             active = histentry.get('active', False)
             url = QUrl.fromEncoded(histentry['url'].encode('ascii'))
             if 'original-url' in histentry:
@@ -360,6 +393,7 @@ class SessionManager(QObject):
             entries.append(entry)
             if active:
                 new_tab.title_changed.emit(histentry['title'])
+
         try:
             new_tab.history.load_items(entries)
         except ValueError as e:
@@ -388,7 +422,7 @@ class SessionManager(QObject):
                                         window=window.win_id)
             tab_to_focus = None
             for i, tab in enumerate(win['tabs']):
-                new_tab = tabbed_browser.tabopen()
+                new_tab = tabbed_browser.tabopen(background=False)
                 self._load_tab(new_tab, tab)
                 if tab.get('active', False):
                     tab_to_focus = i
@@ -460,7 +494,7 @@ class SessionManager(QObject):
 
         Args:
             name: The name of the session. If not given, the session configured
-                  in session_default_name is saved.
+                  in session.default_name is saved.
             current: Save the current session instead of the default.
             quiet: Don't show confirmation message.
             force: Force saving internal sessions (starting with an underline).
diff --git a/qutebrowser/utils/jinja.py b/qutebrowser/utils/jinja.py
index e7b536b60..b6f53645b 100644
--- a/qutebrowser/utils/jinja.py
+++ b/qutebrowser/utils/jinja.py
@@ -136,3 +136,4 @@ def render(template, **kwargs):
 
 
 environment = Environment()
+js_environment = jinja2.Environment(loader=Loader('javascript'))
diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py
index 68cf1d2ba..dc0ff5580 100644
--- a/qutebrowser/utils/log.py
+++ b/qutebrowser/utils/log.py
@@ -95,7 +95,8 @@ LOGGER_NAMES = [
     'commands', 'signals', 'downloads',
     'js', 'qt', 'rfc6266', 'ipc', 'shlexer',
     'save', 'message', 'config', 'sessions',
-    'webelem', 'prompt', 'network', 'sql'
+    'webelem', 'prompt', 'network', 'sql',
+    'greasemonkey'
 ]
 
 
@@ -144,6 +145,7 @@ webelem = logging.getLogger('webelem')
 prompt = logging.getLogger('prompt')
 network = logging.getLogger('network')
 sql = logging.getLogger('sql')
+greasemonkey = logging.getLogger('greasemonkey')
 
 
 ram_handler = None
diff --git a/scripts/dev/ci/travis_backtrace.sh b/scripts/dev/ci/travis_backtrace.sh
index c94d1ff06..227dde8a8 100644
--- a/scripts/dev/ci/travis_backtrace.sh
+++ b/scripts/dev/ci/travis_backtrace.sh
@@ -6,7 +6,7 @@
 
 case $TESTENV in
     py3*-pyqt*)
-        exe=$(readlink -f .tox/$TESTENV/bin/python)
+        exe=$(readlink -f ".tox/$TESTENV/bin/python")
         full=
         ;;
     *)
@@ -15,4 +15,4 @@ case $TESTENV in
         ;;
 esac
 
-find . -name *.core -o -name core -exec gdb --batch --quiet -ex "thread apply all bt $full" "$exe" {} \;
+find . \( -name "*.core" -o -name core \) -exec gdb --batch --quiet -ex "thread apply all bt $full" "$exe" {} \;
diff --git a/scripts/dev/ci/travis_install.sh b/scripts/dev/ci/travis_install.sh
index 4c599aac6..04b118b9b 100644
--- a/scripts/dev/ci/travis_install.sh
+++ b/scripts/dev/ci/travis_install.sh
@@ -21,23 +21,23 @@
 # Stolen from https://github.com/travis-ci/travis-build/blob/master/lib/travis/build/templates/header.sh
 # and adjusted to use ((...))
 travis_retry() {
-    local ANSI_RED="\033[31;1m"
-    local ANSI_RESET="\033[0m"
+    local ANSI_RED='\033[31;1m'
+    local ANSI_RESET='\033[0m'
     local result=0
     local count=1
     while (( count < 3 )); do
         if (( result != 0 )); then
-            echo -e "\n${ANSI_RED}The command \"$@\" failed. Retrying, $count of 3.${ANSI_RESET}\n" >&2
+            echo -e "\\n${ANSI_RED}The command \"$*\" failed. Retrying, $count of 3.${ANSI_RESET}\\n" >&2
         fi
         "$@"
         result=$?
         (( result == 0 )) && break
-        count=$(($count + 1))
+        count=$(( count + 1 ))
         sleep 1
     done
 
     if (( count > 3 )); then
-        echo -e "\n${ANSI_RED}The command \"$@\" failed 3 times.${ANSI_RESET}\n" >&2
+        echo -e "\\n${ANSI_RED}The command \"$*\" failed 3 times.${ANSI_RESET}\\n" >&2
     fi
 
     return $result
@@ -96,6 +96,8 @@ case $TESTENV in
     eslint)
         npm_install eslint
         ;;
+    shellcheck)
+        ;;
     *)
         pip_install pip
         pip_install -r misc/requirements/requirements-tox.txt
diff --git a/scripts/dev/ci/travis_run.sh b/scripts/dev/ci/travis_run.sh
index 2a5424fb9..b7d44968e 100644
--- a/scripts/dev/ci/travis_run.sh
+++ b/scripts/dev/ci/travis_run.sh
@@ -14,6 +14,16 @@ elif [[ $TESTENV == eslint ]]; then
     # travis env
     cd qutebrowser/javascript || exit 1
     eslint --color --report-unused-disable-directives .
+elif [[ $TESTENV == shellcheck ]]; then
+    SCRIPTS=$( mktemp )
+    find scripts/dev/ -name '*.sh' >"$SCRIPTS"
+    find misc/userscripts/ -type f -exec grep -lE '[/ ][bd]ash$|[/ ]sh$|[/ ]ksh$' {} + >>"$SCRIPTS"
+    mapfile -t scripts <"$SCRIPTS"
+    rm -f "$SCRIPTS"
+    docker run \
+           -v "$PWD:/outside" \
+           -w /outside \
+           koalaman/shellcheck:latest "${scripts[@]}"
 else
     args=()
     [[ $TRAVIS_OS_NAME == osx ]] && args=('--qute-bdd-webengine' '--no-xvfb')
diff --git a/scripts/dev/download_release.sh b/scripts/dev/download_release.sh
index 7ec4d9159..207da21c8 100644
--- a/scripts/dev/download_release.sh
+++ b/scripts/dev/download_release.sh
@@ -1,4 +1,5 @@
 #!/bin/bash
+set -e
 
 # This script downloads the given release from GitHub so we can mirror it on
 # qutebrowser.org.
@@ -16,16 +17,16 @@ mkdir windows
 
 base="https://github.com/qutebrowser/qutebrowser/releases/download/v$1"
 
-wget "$base/qutebrowser-$1.tar.gz" || exit 1
-wget "$base/qutebrowser-$1.tar.gz.asc" || exit 1
-wget "$base/qutebrowser-$1.dmg" || exit 1
-wget "$base/qutebrowser_${1}-1_all.deb" || exit 1
+wget "$base/qutebrowser-$1.tar.gz"
+wget "$base/qutebrowser-$1.tar.gz.asc"
+wget "$base/qutebrowser-$1.dmg"
+wget "$base/qutebrowser_${1}-1_all.deb"
 
 cd windows
-wget "$base/qutebrowser-${1}-amd64.msi" || exit 1
-wget "$base/qutebrowser-${1}-win32.msi" || exit 1
-wget "$base/qutebrowser-${1}-windows-standalone-amd64.zip" || exit 1
-wget "$base/qutebrowser-${1}-windows-standalone-win32.zip" || exit 1
+wget "$base/qutebrowser-${1}-amd64.msi"
+wget "$base/qutebrowser-${1}-win32.msi"
+wget "$base/qutebrowser-${1}-windows-standalone-amd64.zip"
+wget "$base/qutebrowser-${1}-windows-standalone-win32.zip"
 
 dest="/srv/http/qutebrowser/releases/v$1"
 cd "$oldpwd"
diff --git a/scripts/dev/quit_segfault_test.sh b/scripts/dev/quit_segfault_test.sh
index 655eb262a..389f125b9 100755
--- a/scripts/dev/quit_segfault_test.sh
+++ b/scripts/dev/quit_segfault_test.sh
@@ -1,14 +1,12 @@
-#!/bin/bash
+#!/usr/bin/env bash
 
-if [[ $PWD == */scripts ]]; then
-    cd ..
-fi
+[[ $PWD == */scripts ]] && cd ..
 
 echo > crash.log
 while :; do
     exit=0
-    while (( $exit == 0)); do
-        duration=$(($RANDOM%10000))
+    while (( exit == 0 )); do
+        duration=$(( RANDOM % 10000 ))
         python3 -m qutebrowser --debug ":later $duration quit" http://www.heise.de/
         exit=$?
     done
diff --git a/scripts/hist_importer.py b/scripts/hist_importer.py
new file mode 100755
index 000000000..f4ad47062
--- /dev/null
+++ b/scripts/hist_importer.py
@@ -0,0 +1,154 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+# Copyright 2017 Josefson Souza <josefson.br@gmail.com>
+
+# This file is part of qutebrowser.
+#
+# qutebrowser is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# qutebrowser is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with qutebrowser.  If not, see <http://www.gnu.org/licenses/>.
+
+
+"""Tool to import browser history from other browsers."""
+
+
+import argparse
+import sqlite3
+import sys
+import os
+
+
+def parse():
+    """Parse command line arguments."""
+    description = ("This program is meant to extract browser history from your"
+                   " previous browser and import them into qutebrowser.")
+    epilog = ("Databases:\n\n\tqutebrowser: Is named 'history.sqlite' and can "
+              "be found at your --basedir. In order to find where your "
+              "basedir is you can run ':open qute:version' inside qutebrowser."
+              "\n\n\tFirefox: Is named 'places.sqlite', and can be found at "
+              "your system's profile folder. Check this link for where it is "
+              "located: http://kb.mozillazine.org/Profile_folder"
+              "\n\n\tChrome: Is named 'History', and can be found at the "
+              "respective User Data Directory. Check this link for where it is"
+              "located: https://chromium.googlesource.com/chromium/src/+/"
+              "master/docs/user_data_dir.md\n\n"
+              "Example: hist_importer.py -b firefox -s /Firefox/Profile/"
+              "places.sqlite -d /qutebrowser/data/history.sqlite")
+    parser = argparse.ArgumentParser(
+        description=description, epilog=epilog,
+        formatter_class=argparse.RawTextHelpFormatter
+    )
+    parser.add_argument('-b', '--browser', dest='browser', required=True,
+                        type=str, help='Browsers: {firefox, chrome}')
+    parser.add_argument('-s', '--source', dest='source', required=True,
+                        type=str, help='Source: Full path to the sqlite data'
+                        'base file from the source browser.')
+    parser.add_argument('-d', '--dest', dest='dest', required=True, type=str,
+                        help='\nDestination: Full path to the qutebrowser '
+                        'sqlite database')
+    return parser.parse_args()
+
+
+def open_db(data_base):
+    """Open connection with database."""
+    if os.path.isfile(data_base):
+        conn = sqlite3.connect(data_base)
+        return conn
+    else:
+        sys.exit('The file {} does not exist.'.format(data_base))
+
+
+def extract(source, query):
+    """Get records from source database.
+
+    Args:
+        source: File path to the source database where we want to extract the
+        data from.
+        query: The query string to be executed in order to retrieve relevant
+        attributes as (datetime, url, time) from the source database according
+        to the browser chosen.
+    """
+    try:
+        conn = open_db(source)
+        cursor = conn.cursor()
+        cursor.execute(query)
+        history = cursor.fetchall()
+        conn.close()
+        return history
+    except sqlite3.OperationalError as op_e:
+        sys.exit('Could not perform queries on the source database: '
+                 '{}'.format(op_e))
+
+
+def clean(history):
+    """Clean up records from source database.
+
+    Receives a list of record and sanityze them in order for them to be
+    properly imported to qutebrowser. Sanitation requires addiing a 4th
+    attribute 'redirect' which is filled with '0's, and also purging all
+    records that have a NULL/None datetime attribute.
+
+    Args:
+        history: List of records (datetime, url, title) from source database.
+    """
+    nulls = [record for record in history if record[0] is None]
+    for null_datetime in nulls:
+        history.remove(null_datetime)
+    history = [list(record) for record in history]
+    for record in history:
+        record.append('0')
+    return history
+
+
+def insert_qb(history, dest):
+    """Insert history into dest database.
+
+    Args:
+        history: List of records.
+        dest: File path to the destination database, where history will be
+        inserted.
+    """
+    conn = open_db(dest)
+    cursor = conn.cursor()
+    cursor.executemany(
+        'INSERT INTO History (url,title,atime,redirect) VALUES (?,?,?,?)',
+        history
+    )
+    cursor.execute('DROP TABLE CompletionHistory')
+    conn.commit()
+    conn.close()
+
+
+def main():
+    """Main control flux of the script."""
+    args = parse()
+    browser = args.browser.lower()
+    source, dest = args.source, args.dest
+    query = {
+        'firefox': 'select url,title,last_visit_date/1000000 as date '
+                   'from moz_places',
+        'chrome': 'select url,title,last_visit_time/10000000 as date '
+                  'from urls',
+    }
+    if browser not in query:
+        sys.exit('Sorry, the selected browser: "{}" is not supported.'.format(
+            browser))
+    else:
+        history = extract(source, query[browser])
+        history = clean(history)
+        insert_qb(history, dest)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tests/end2end/features/downloads.feature b/tests/end2end/features/downloads.feature
index 69f47603b..833f63cb4 100644
--- a/tests/end2end/features/downloads.feature
+++ b/tests/end2end/features/downloads.feature
@@ -536,7 +536,7 @@ Feature: Downloading things from a website.
         And I open data/downloads/download.bin without waiting
         And I wait for the download prompt for "*"
         And I run :prompt-accept (tmpdir)(dirsep)downloads
-        And I open data/downloads/download.bin without waiting
+        And I open data/downloads/download2.bin without waiting
         And I wait for the download prompt for "*"
         And I directly open the download
         And I open data/downloads/download.bin without waiting
diff --git a/tests/end2end/features/editor.feature b/tests/end2end/features/editor.feature
index 21f3df425..15da4a6cd 100644
--- a/tests/end2end/features/editor.feature
+++ b/tests/end2end/features/editor.feature
@@ -116,7 +116,8 @@ Feature: Opening external editors
         Then the javascript message "text: foobar" should be logged
 
     # Could not get signals working on Windows
-    @posix
+    # There's no guarantee that the tab gets deleted...
+    @posix @flaky
     Scenario: Spawning an editor and closing the tab
         When I set up a fake editor that waits
         And I open data/editor.html
diff --git a/tests/end2end/features/javascript.feature b/tests/end2end/features/javascript.feature
index a309d6187..3ccd50efb 100644
--- a/tests/end2end/features/javascript.feature
+++ b/tests/end2end/features/javascript.feature
@@ -123,3 +123,25 @@ Feature: Javascript stuff
         And I wait for "[*/data/javascript/windowsize.html:*] loaded" in the log
         And I run :tab-next
         Then the window sizes should be the same
+
+    Scenario: Have a GreaseMonkey script run at page start
+        When I have a GreaseMonkey file saved for document-start with noframes unset
+        And I run :greasemonkey-reload
+        And I open data/hints/iframe.html
+        # This second reload is required in webengine < 5.8 for scripts
+        # registered to run at document-start, some sort of timing issue.
+        And I run :reload
+        Then the javascript message "Script is running on /data/hints/iframe.html" should be logged
+
+    Scenario: Have a GreaseMonkey script running on frames
+        When I have a GreaseMonkey file saved for document-end with noframes unset
+        And I run :greasemonkey-reload
+        And I open data/hints/iframe.html
+        Then the javascript message "Script is running on /data/hints/html/wrapped.html" should be logged
+
+    @flaky
+    Scenario: Have a GreaseMonkey script running on noframes
+        When I have a GreaseMonkey file saved for document-end with noframes set
+        And I run :greasemonkey-reload
+        And I open data/hints/iframe.html
+        Then the javascript message "Script is running on /data/hints/html/wrapped.html" should not be logged
diff --git a/tests/end2end/features/qutescheme.feature b/tests/end2end/features/qutescheme.feature
index 51db7f767..1f13a8ac1 100644
--- a/tests/end2end/features/qutescheme.feature
+++ b/tests/end2end/features/qutescheme.feature
@@ -113,6 +113,8 @@ Feature: Special qute:// pages
         And I wait for "Config option changed: ignore_case *" in the log
         Then the option ignore_case should be set to always
 
+    # Sometimes, an unrelated value gets set
+    @flaky
     Scenario: Focusing input fields in qute://settings and entering invalid value
         When I open qute://settings
         # scroll to the right - the table does not fit in the default screen
diff --git a/tests/end2end/features/search.feature b/tests/end2end/features/search.feature
index 3778f963d..56fcca207 100644
--- a/tests/end2end/features/search.feature
+++ b/tests/end2end/features/search.feature
@@ -225,11 +225,15 @@ Feature: Searching on a page
         Then the following tabs should be open:
             - data/search.html (active)
 
+    # Following a link selected via JS doesn't work in Qt 5.10 anymore.
+    @qt!=5.10
     Scenario: Follow a manually selected link
         When I run :jseval --file (testdata)/search_select.js
         And I run :follow-selected
         Then data/hello.txt should be loaded
 
+    # Following a link selected via JS doesn't work in Qt 5.10 anymore.
+    @qt!=5.10
     Scenario: Follow a manually selected link in a new tab
         When I run :window-only
         And I run :jseval --file (testdata)/search_select.js
diff --git a/tests/end2end/features/test_javascript_bdd.py b/tests/end2end/features/test_javascript_bdd.py
index 9f6c021ce..8f69ef6d4 100644
--- a/tests/end2end/features/test_javascript_bdd.py
+++ b/tests/end2end/features/test_javascript_bdd.py
@@ -17,6 +17,8 @@
 # You should have received a copy of the GNU General Public License
 # along with qutebrowser.  If not, see <http://www.gnu.org/licenses/>.
 
+import os.path
+
 import pytest_bdd as bdd
 bdd.scenarios('javascript.feature')
 
@@ -29,3 +31,38 @@ def check_window_sizes(quteproc):
     hidden_size = hidden.message.split()[-1]
     visible_size = visible.message.split()[-1]
     assert hidden_size == visible_size
+
+
+test_gm_script = r"""
+// ==UserScript==
+// @name qutebrowser test userscript
+// @namespace invalid.org
+// @include http://localhost:*/data/hints/iframe.html
+// @include http://localhost:*/data/hints/html/wrapped.html
+// @exclude ???
+// @run-at {stage}
+// {frames}
+// ==/UserScript==
+console.log("Script is running on " + window.location.pathname);
+"""
+
+
+@bdd.when(bdd.parsers.parse("I have a GreaseMonkey file saved for {stage} "
+                            "with noframes {frameset}"))
+def create_greasemonkey_file(quteproc, stage, frameset):
+    script_path = os.path.join(quteproc.basedir, 'data', 'greasemonkey')
+    try:
+        os.mkdir(script_path)
+    except FileExistsError:
+        pass
+    file_path = os.path.join(script_path, 'test.user.js')
+    if frameset == "set":
+        frames = "@noframes"
+    elif frameset == "unset":
+        frames = ""
+    else:
+        raise ValueError("noframes can only be set or unset, "
+                         "not {}".format(frameset))
+    with open(file_path, 'w', encoding='utf-8') as f:
+        f.write(test_gm_script.format(stage=stage,
+                                     frames=frames))
diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py
index 27c347ca4..a0b48ab4b 100644
--- a/tests/end2end/fixtures/quteprocess.py
+++ b/tests/end2end/fixtures/quteprocess.py
@@ -36,7 +36,7 @@ import pytest
 from PyQt5.QtCore import pyqtSignal, QUrl, qVersion
 
 from qutebrowser.misc import ipc
-from qutebrowser.utils import log, utils, javascript
+from qutebrowser.utils import log, utils, javascript, qtutils
 from helpers import utils as testutils
 from end2end.fixtures import testprocess
 
@@ -527,6 +527,7 @@ class QuteProc(testprocess.Process):
         super().before_test()
         self.send_cmd(':config-clear')
         self._init_settings()
+        self.clear_data()
 
     def _init_settings(self):
         """Adjust some qutebrowser settings after starting."""
@@ -687,9 +688,12 @@ class QuteProc(testprocess.Process):
             raise ValueError("Invalid URL {}: {}".format(url,
                                                          qurl.errorString()))
 
-        if qurl == QUrl('about:blank'):
+        if (qurl == QUrl('about:blank') and
+                not qtutils.version_check('5.10', compiled=False)):
             # For some reason, we don't get a LoadStatus.success for
             # about:blank sometimes.
+            # However, if we do this for Qt 5.10, we get general testsuite
+            # instability as site loads get reported with about:blank...
             pattern = "Changing title for idx * to 'about:blank'"
         else:
             # We really need the same representation that the webview uses in
diff --git a/tests/end2end/fixtures/testprocess.py b/tests/end2end/fixtures/testprocess.py
index a4b136193..bc987043f 100644
--- a/tests/end2end/fixtures/testprocess.py
+++ b/tests/end2end/fixtures/testprocess.py
@@ -333,7 +333,7 @@ class Process(QObject):
         else:
             return value == expected
 
-    def _wait_for_existing(self, override_waited_for, **kwargs):
+    def _wait_for_existing(self, override_waited_for, after, **kwargs):
         """Check if there are any line in the history for wait_for.
 
         Return: either the found line or None.
@@ -345,7 +345,15 @@ class Process(QObject):
                 value = getattr(line, key)
                 matches.append(self._match_data(value, expected))
 
-            if all(matches) and (not line.waited_for or override_waited_for):
+            if after is None:
+                too_early = False
+            else:
+                too_early = ((line.timestamp, line.msecs) <
+                             (after.timestamp, after.msecs))
+
+            if (all(matches) and
+                    (not line.waited_for or override_waited_for) and
+                    not too_early):
                 # If we waited for this line, chances are we don't mean the
                 # same thing the next time we use wait_for and it matches
                 # this line again.
@@ -363,7 +371,7 @@ class Process(QObject):
         __tracebackhide__ = lambda e: e.errisinstance(WaitForTimeout)
         message = kwargs.get('message', None)
         if message is not None:
-            elided = quteutils.elide(repr(message), 50)
+            elided = quteutils.elide(repr(message), 100)
             self._log("\n----> Waiting for {} in the log".format(elided))
 
         spy = QSignalSpy(self.new_data)
@@ -422,7 +430,7 @@ class Process(QObject):
         pass
 
     def wait_for(self, timeout=None, *, override_waited_for=False,
-                 do_skip=False, divisor=1, **kwargs):
+                 do_skip=False, divisor=1, after=None, **kwargs):
         """Wait until a given value is found in the data.
 
         Keyword arguments to this function get interpreted as attributes of the
@@ -435,6 +443,7 @@ class Process(QObject):
                                  again.
             do_skip: If set, call pytest.skip on a timeout.
             divisor: A factor to decrease the timeout by.
+            after: If it's an existing line, ensure it's after the given one.
 
         Return:
             The matched line.
@@ -456,7 +465,8 @@ class Process(QObject):
         for key in kwargs:
             assert key in self.KEYS
 
-        existing = self._wait_for_existing(override_waited_for, **kwargs)
+        existing = self._wait_for_existing(override_waited_for, after,
+                                           **kwargs)
         if existing is not None:
             return existing
         else:
diff --git a/tests/unit/completion/test_histcategory.py b/tests/unit/completion/test_histcategory.py
index 834b3a5a3..b87eb6ac2 100644
--- a/tests/unit/completion/test_histcategory.py
+++ b/tests/unit/completion/test_histcategory.py
@@ -78,6 +78,10 @@ def hist(init_sql, config_stub):
     ("can't",
      [("can't touch this", ''), ('a', '')],
      [("can't touch this", '')]),
+
+    ("ample itle",
+     [('example.com', 'title'), ('example.com', 'nope')],
+     [('example.com', 'title')]),
 ])
 def test_set_pattern(pattern, before, after, model_validator, hist):
     """Validate the filtering and sorting results of set_pattern."""
diff --git a/tests/unit/javascript/test_greasemonkey.py b/tests/unit/javascript/test_greasemonkey.py
new file mode 100644
index 000000000..0f5fe476c
--- /dev/null
+++ b/tests/unit/javascript/test_greasemonkey.py
@@ -0,0 +1,104 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+# Copyright 2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+
+# This file is part of qutebrowser.
+#
+# qutebrowser is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# qutebrowser is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with qutebrowser.  If not, see <http://www.gnu.org/licenses/>.
+
+"""Tests for qutebrowser.browser.greasemonkey."""
+
+import logging
+
+import pytest
+import py.path  # pylint: disable=no-name-in-module
+from PyQt5.QtCore import QUrl
+
+from qutebrowser.browser import greasemonkey
+
+test_gm_script = """
+// ==UserScript==
+// @name qutebrowser test userscript
+// @namespace invalid.org
+// @include http://localhost:*/data/title.html
+// @match http://trolol*
+// @exclude https://badhost.xxx/*
+// @run-at document-start
+// ==/UserScript==
+console.log("Script is running.");
+"""
+
+pytestmark = pytest.mark.usefixtures('data_tmpdir')
+
+
+def _save_script(script_text, filename):
+    # pylint: disable=no-member
+    file_path = py.path.local(greasemonkey._scripts_dir()) / filename
+    # pylint: enable=no-member
+    file_path.write_text(script_text, encoding='utf-8', ensure=True)
+
+
+def test_all():
+    """Test that a script gets read from file, parsed and returned."""
+    _save_script(test_gm_script, 'test.user.js')
+
+    gm_manager = greasemonkey.GreasemonkeyManager()
+    assert (gm_manager.all_scripts()[0].name ==
+            "qutebrowser test userscript")
+
+
+@pytest.mark.parametrize("url, expected_matches", [
+    # included
+    ('http://trololololololo.com/', 1),
+    # neither included nor excluded
+    ('http://aaaaaaaaaa.com/', 0),
+    # excluded
+    ('https://badhost.xxx/', 0),
+])
+def test_get_scripts_by_url(url, expected_matches):
+    """Check Greasemonkey include/exclude rules work."""
+    _save_script(test_gm_script, 'test.user.js')
+    gm_manager = greasemonkey.GreasemonkeyManager()
+
+    scripts = gm_manager.scripts_for(QUrl(url))
+    assert (len(scripts.start + scripts.end + scripts.idle) ==
+            expected_matches)
+
+
+def test_no_metadata(caplog):
+    """Run on all sites at document-end is the default."""
+    _save_script("var nothing = true;\n", 'nothing.user.js')
+
+    with caplog.at_level(logging.WARNING):
+        gm_manager = greasemonkey.GreasemonkeyManager()
+
+    scripts = gm_manager.scripts_for(QUrl('http://notamatch.invalid/'))
+    assert len(scripts.start + scripts.end + scripts.idle) == 1
+    assert len(scripts.end) == 1
+
+
+def test_bad_scheme(caplog):
+    """qute:// isn't in the list of allowed schemes."""
+    _save_script("var nothing = true;\n", 'nothing.user.js')
+
+    with caplog.at_level(logging.WARNING):
+        gm_manager = greasemonkey.GreasemonkeyManager()
+
+    scripts = gm_manager.scripts_for(QUrl('qute://settings'))
+    assert len(scripts.start + scripts.end + scripts.idle) == 0
+
+
+def test_load_emits_signal(qtbot):
+    gm_manager = greasemonkey.GreasemonkeyManager()
+    with qtbot.wait_signal(gm_manager.scripts_reloaded):
+        gm_manager.load_scripts()
diff --git a/tests/unit/misc/test_sessions.py b/tests/unit/misc/test_sessions.py
index 771430d5b..b2cb8a3dd 100644
--- a/tests/unit/misc/test_sessions.py
+++ b/tests/unit/misc/test_sessions.py
@@ -170,7 +170,7 @@ class TestSaveAll:
 ])
 def test_get_session_name(config_stub, sess_man, arg, config, current,
                           expected):
-    config_stub.val.session_default_name = config
+    config_stub.val.session.default_name = config
     sess_man._current = current
     assert sess_man._get_session_name(arg) == expected
 
diff --git a/tox.ini b/tox.ini
index e59e8b660..5b8bc05b5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -181,6 +181,7 @@ commands =
 [testenv:eslint]
 # This is duplicated in travis_run.sh for Travis CI because we can't get tox in
 # the JavaScript environment easily.
+basepython = python3
 deps =
 whitelist_externals = eslint
 changedir = {toxinidir}/qutebrowser/javascript