Resources chevron_right Protocols chevron_right MinCED
Protocol // Defense Systems

MinCED

Identify CRISPR spacers in FASTA sequences and match them against viral databases to infer host–virus interactions.

Defense Systems codeGitHub scheduleApril 3, 2026

Installation

We first need to clone the GitHub repository of the program. Go to github.com/ctSkennerton/minced, click the green Code button and copy the HTTPS address. Then enter the following command in the location you want to install the program (there is no need to create a new folder):

git clone https://github.com/ctSkennerton/minced.git

Inside the folder where the repo was cloned, run:

make

In case some Java components are missing, install them:

sudo apt-get install default-jdk

Alternative installation

conda install bioconda::minced

Running

Import the folder containing the program to the PATH

export PATH="/home/minced:/$PATH"
info

Always keep the files minced and minced.jar in the same folder.

When looking for CRISPR, always use a nucleotide sequence as input.

./minced -gffFull -spacers Genome.fna CRISPR_Genome.txt CRISPR_Genome.gff

Three files will be generated:

File Description
CRISPR_Genome.gff GFF annotation file for the detected CRISPR arrays
CRISPR_Genome.fa FASTA sequences of the spacers and the scaffold they came from
CRISPR_Genome.txt Position and length of the spacers and repeats

Useful commands

./minced --help

MinCED, a program to find CRISPRs in shotgun DNA sequences or full genomes

Usage:    minced [options] file.fa [outputFile.txt] [outputFile.gff]

Options:  -searchWL  Length of search window used to discover CRISPRs (range: 6-9). Default: 8
          -minNR     Minimum number of repeats a CRISPR must contain. Default: 3
          -minRL     Minimum length of the CRISPR repeats. Default: 23
          -maxRL     Maximum length of the CRISPR repeats. Default: 47
          -minSL     Minimum length of the CRISPR spacers. Default: 26
          -maxSL     Maximum length of the CRISPR spacers. Default: 50
          -gff       Output summary results in gff format containing
                     only the positions of the CRISPR arrays. Default: false
          -gffFull   Output detailed results in gff format containing
                     positions of CRISPR arrays and all repeat units. Default: false
          -spacers   Output a fasta formatted file containing the spacers. Default: false
          -h --help  Output this handy help message
          --version  Output version information
./minced --version

Identifying Viruses Matching the Spacers

Create a BLASTn database

Create a BLASTn database with the dataset you want to search for spacer matches:

makeblastdb -in IMGVR_viral_genomes.fna -dbtype nucl -parse_seqids

Running BLASTn against database

Use the blastn-short task — better suited for short sequences like spacers. The spacers are used as the query, and the viral dataset as the database.

blastn -task blastn-short -query minced-spacer.fa -db IMGVR_viral_genomes.fna -word_size 7 -perc_identity 100 -qcov_hsp_perc 100 -ungapped -dust no -soft_masking false -evalue 1000 -num_threads 10 -max_hsps 1 -outfmt "6 qseqid sseqid pident length mismatch gapopen qstart qend sstart send sstrand evalue bitscore" -out Blast_spacers_vs_IMGVR.tab

Running BLASTn against a genome

To check if your spacers match a specific genome, use the -subject flag instead of creating a database:

blastn -task blastn-short -query minced-spacer.fa -subject target_Genomes.fna -word_size 7 -perc_identity 100 -qcov_hsp_perc 100 -ungapped -max_target_seqs 100 -outfmt "6 qseqid sseqid pident length mismatch gapopen qstart qend sstart send sstrand evalue bitscore" -num_threads 10 -out Blast_spacers_vs_genome.tab

Curating the Results

Acquire spacer lengths

First we need the spacer length information. Run this on the FASTA file containing the spacer sequences:

seqkit fx2tab --length --name --header-line minced-spacer.fa >> minced-spacer_lenght.tab

Length of spacers and matches

Now we can check features of the BLASTn results to confirm true positives. The following script checks these parameters:

Parameter Criteria
Full-length alignment Alignment length equals spacer length (perfect) or shorter by 1 nt (ok)
Number of mismatches 0 mismatches (perfect) or 1 mismatch (ok)
No gaps gapopen = 0
#!/usr/bin/env python3
"""
Filter BLASTn spacer hits using spacer lengths + quality rules.
Automatically adds BLAST header if missing.

Expected BLAST outfmt:
6 qseqid sseqid pident length mismatch gapopen qstart qend sstart send sstrand evalue bitscore
"""

import argparse
import sys
import pandas as pd
from pathlib import Path


BLAST_HEADER = [
    "qseqid", "sseqid", "pident", "length", "mismatch", "gapopen",
    "qstart", "qend", "sstart", "send", "sstrand", "evalue", "bitscore"
]


def ensure_blast_header(blast_path: Path):
    """
    Ensure BLAST outfmt 6 file has the correct header.
    If missing, prepend it in-place.
    """
    with blast_path.open() as f:
        first_line = f.readline().strip()

    # If header already present, do nothing
    if first_line.split("\t") == BLAST_HEADER:
        return

    # Otherwise, prepend header
    tmp_path = blast_path.with_suffix(".tmp")
    with blast_path.open() as fin, tmp_path.open("w") as fout:
        fout.write("\t".join(BLAST_HEADER) + "\n")
        for line in fin:
            fout.write(line)

    tmp_path.replace(blast_path)


def read_spacer_lengths(path: str) -> pd.DataFrame:
    df = pd.read_csv(
        path,
        sep="\t",
        comment="#",
        header=None,
        names=["qseqid", "spacer_len"],
        dtype={"qseqid": str}
    )
    df["spacer_len"] = pd.to_numeric(df["spacer_len"], errors="coerce")
    df = df.dropna(subset=["spacer_len"]).copy()
    df["spacer_len"] = df["spacer_len"].astype(int)
    return df


def read_blast(path: str) -> pd.DataFrame:
    df = pd.read_csv(path, sep="\t", dtype=str)

    numeric_cols = ["pident", "length", "mismatch", "gapopen", "qstart",
                    "qend", "sstart", "send", "evalue", "bitscore"]
    for col in numeric_cols:
        df[col] = pd.to_numeric(df[col], errors="coerce")

    return df.dropna(subset=["length", "mismatch", "gapopen"])


def classify_len(aln_len, spacer_len):
    if aln_len == spacer_len:
        return "perfect"
    if aln_len == spacer_len - 1:
        return "ok"
    return None


def classify_mismatch(mm):
    if mm == 0:
        return "perfect"
    if mm == 1:
        return "ok"
    return None


def main():
    ap = argparse.ArgumentParser(
        description="Add BLAST header (if missing) and filter spacer BLAST hits."
    )
    ap.add_argument("-b", "--blast", required=True, help="BLAST outfmt 6 file")
    ap.add_argument("-l", "--lengths", required=True, help="Spacer lengths table")
    ap.add_argument("-o", "--out", default="-", help="Output TSV (default: stdout)")
    ap.add_argument("--reason-col", default="selection_reason",
                    help="Name of the added reason column")
    args = ap.parse_args()

    blast_path = Path(args.blast)

    # 1) Ensure header exists
    ensure_blast_header(blast_path)

    # 2) Read inputs
    blast = read_blast(blast_path)
    lengths = read_spacer_lengths(args.lengths)

    df = blast.merge(lengths, on="qseqid", how="inner")

    # 3) Apply filters
    df["len_class"] = df.apply(
        lambda r: classify_len(int(r["length"]), int(r["spacer_len"])), axis=1
    )
    df["mm_class"] = df["mismatch"].apply(classify_mismatch)

    passed = df[
        (df["gapopen"] == 0) &
        (df["len_class"].isin(["perfect", "ok"])) &
        (df["mm_class"].isin(["perfect", "ok"]))
    ].copy()

    # 4) Build reason column
    passed[args.reason_col] = (
        "len:" + passed["len_class"] +
        ";mismatch:" + passed["mm_class"] +
        ";gap:perfect"
    )

    # 5) Output: BLAST columns + reason
    out_cols = BLAST_HEADER + [args.reason_col]
    passed = passed[out_cols]

    if args.out == "-" or args.out.lower() == "stdout":
        passed.to_csv(sys.stdout, sep="\t", index=False)
    else:
        passed.to_csv(args.out, sep="\t", index=False)


if __name__ == "__main__":
    main()

Transform this script into an executable:

chmod +x Spacer-match_check.sh

Run the script to generate your final output:

python3 Spacer-match_check.sh -b Blast_spacers_vs_genome.tab -l minced-spacer_lenght.tab -o Checked_Blast_spacers_vs_genome.tab

Extra Thoughts

Spacers belonging to a CRISPR array

If a scaffold has a 100% match and 100% query coverage (qend − qstart) with a spacer, there are two options:

  • This assembly belongs to a MGE
  • This assembly is part of a CRISPR array

There are 2 ways to check this:

  • See if that scaffold was used in the binning process to form a MAG — if it belongs to a MAG it is likely from a CRISPR array (but not definitive, it can also be a provirus).
  • Run MinCED on these scaffolds to see if a CRISPR array organisation is detected.
info

If the scaffolds do not belong to a MAG and no CRISPR array is detected, treat them as possible MGEs and proceed to the virus annotation pipeline.